Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/services/abstractCodeEditorService.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../base/browser/dom.js';
7
import * as domStylesheets from '../../../base/browser/domStylesheets.js';
8
import * as cssJs from '../../../base/browser/cssValue.js';
9
import { Emitter, Event } from '../../../base/common/event.js';
10
import { IDisposable, DisposableStore, Disposable, toDisposable, DisposableMap } from '../../../base/common/lifecycle.js';
11
import { LinkedList } from '../../../base/common/linkedList.js';
12
import * as strings from '../../../base/common/strings.js';
13
import { URI } from '../../../base/common/uri.js';
14
import { ICodeEditor, IDiffEditor } from '../editorBrowser.js';
15
import { ICodeEditorOpenHandler, ICodeEditorService } from './codeEditorService.js';
16
import { IContentDecorationRenderOptions, IDecorationRenderOptions, IThemeDecorationRenderOptions, isThemeColor } from '../../common/editorCommon.js';
17
import { IModelDecorationOptions, IModelDecorationOverviewRulerOptions, InjectedTextOptions, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from '../../common/model.js';
18
import { IResourceEditorInput } from '../../../platform/editor/common/editor.js';
19
import { IColorTheme, IThemeService } from '../../../platform/theme/common/themeService.js';
20
import { ThemeColor } from '../../../base/common/themables.js';
21
22
export abstract class AbstractCodeEditorService extends Disposable implements ICodeEditorService {
23
24
declare readonly _serviceBrand: undefined;
25
26
private readonly _onWillCreateCodeEditor = this._register(new Emitter<void>());
27
public readonly onWillCreateCodeEditor = this._onWillCreateCodeEditor.event;
28
29
private readonly _onCodeEditorAdd: Emitter<ICodeEditor> = this._register(new Emitter<ICodeEditor>());
30
public readonly onCodeEditorAdd: Event<ICodeEditor> = this._onCodeEditorAdd.event;
31
32
private readonly _onCodeEditorRemove: Emitter<ICodeEditor> = this._register(new Emitter<ICodeEditor>());
33
public readonly onCodeEditorRemove: Event<ICodeEditor> = this._onCodeEditorRemove.event;
34
35
private readonly _onWillCreateDiffEditor = this._register(new Emitter<void>());
36
public readonly onWillCreateDiffEditor = this._onWillCreateDiffEditor.event;
37
38
private readonly _onDiffEditorAdd: Emitter<IDiffEditor> = this._register(new Emitter<IDiffEditor>());
39
public readonly onDiffEditorAdd: Event<IDiffEditor> = this._onDiffEditorAdd.event;
40
41
private readonly _onDiffEditorRemove: Emitter<IDiffEditor> = this._register(new Emitter<IDiffEditor>());
42
public readonly onDiffEditorRemove: Event<IDiffEditor> = this._onDiffEditorRemove.event;
43
44
private readonly _onDidChangeTransientModelProperty: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());
45
public readonly onDidChangeTransientModelProperty: Event<ITextModel> = this._onDidChangeTransientModelProperty.event;
46
47
protected readonly _onDecorationTypeRegistered: Emitter<string> = this._register(new Emitter<string>());
48
public onDecorationTypeRegistered: Event<string> = this._onDecorationTypeRegistered.event;
49
50
private readonly _codeEditors: { [editorId: string]: ICodeEditor };
51
private readonly _diffEditors: { [editorId: string]: IDiffEditor };
52
protected _globalStyleSheet: GlobalStyleSheet | null;
53
private readonly _decorationOptionProviders = new Map<string, IModelDecorationOptionsProvider>();
54
private readonly _editorStyleSheets = new Map<string, RefCountedStyleSheet>();
55
private readonly _codeEditorOpenHandlers = new LinkedList<ICodeEditorOpenHandler>();
56
57
constructor(
58
@IThemeService private readonly _themeService: IThemeService,
59
) {
60
super();
61
this._codeEditors = Object.create(null);
62
this._diffEditors = Object.create(null);
63
this._globalStyleSheet = null;
64
}
65
66
willCreateCodeEditor(): void {
67
this._onWillCreateCodeEditor.fire();
68
}
69
70
addCodeEditor(editor: ICodeEditor): void {
71
this._codeEditors[editor.getId()] = editor;
72
this._onCodeEditorAdd.fire(editor);
73
}
74
75
removeCodeEditor(editor: ICodeEditor): void {
76
if (delete this._codeEditors[editor.getId()]) {
77
this._onCodeEditorRemove.fire(editor);
78
}
79
}
80
81
listCodeEditors(): ICodeEditor[] {
82
return Object.keys(this._codeEditors).map(id => this._codeEditors[id]);
83
}
84
85
willCreateDiffEditor(): void {
86
this._onWillCreateDiffEditor.fire();
87
}
88
89
addDiffEditor(editor: IDiffEditor): void {
90
this._diffEditors[editor.getId()] = editor;
91
this._onDiffEditorAdd.fire(editor);
92
}
93
94
removeDiffEditor(editor: IDiffEditor): void {
95
if (delete this._diffEditors[editor.getId()]) {
96
this._onDiffEditorRemove.fire(editor);
97
}
98
}
99
100
listDiffEditors(): IDiffEditor[] {
101
return Object.keys(this._diffEditors).map(id => this._diffEditors[id]);
102
}
103
104
getFocusedCodeEditor(): ICodeEditor | null {
105
let editorWithWidgetFocus: ICodeEditor | null = null;
106
107
const editors = this.listCodeEditors();
108
for (const editor of editors) {
109
110
if (editor.hasTextFocus()) {
111
// bingo!
112
return editor;
113
}
114
115
if (editor.hasWidgetFocus()) {
116
editorWithWidgetFocus = editor;
117
}
118
}
119
120
return editorWithWidgetFocus;
121
}
122
123
124
private _getOrCreateGlobalStyleSheet(): GlobalStyleSheet {
125
if (!this._globalStyleSheet) {
126
this._globalStyleSheet = this._createGlobalStyleSheet();
127
}
128
return this._globalStyleSheet;
129
}
130
131
protected _createGlobalStyleSheet(): GlobalStyleSheet {
132
return new GlobalStyleSheet(domStylesheets.createStyleSheet());
133
}
134
135
private _getOrCreateStyleSheet(editor: ICodeEditor | undefined): GlobalStyleSheet | RefCountedStyleSheet {
136
if (!editor) {
137
return this._getOrCreateGlobalStyleSheet();
138
}
139
const domNode = editor.getContainerDomNode();
140
if (!dom.isInShadowDOM(domNode)) {
141
return this._getOrCreateGlobalStyleSheet();
142
}
143
const editorId = editor.getId();
144
if (!this._editorStyleSheets.has(editorId)) {
145
const refCountedStyleSheet = new RefCountedStyleSheet(this, editorId, domStylesheets.createStyleSheet(domNode));
146
this._editorStyleSheets.set(editorId, refCountedStyleSheet);
147
}
148
return this._editorStyleSheets.get(editorId)!;
149
}
150
151
_removeEditorStyleSheets(editorId: string): void {
152
this._editorStyleSheets.delete(editorId);
153
}
154
155
public registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): IDisposable {
156
let provider = this._decorationOptionProviders.get(key);
157
if (!provider) {
158
const styleSheet = this._getOrCreateStyleSheet(editor);
159
const providerArgs: ProviderArguments = {
160
styleSheet: styleSheet,
161
key: key,
162
parentTypeKey: parentTypeKey,
163
options: options || Object.create(null)
164
};
165
if (!parentTypeKey) {
166
provider = new DecorationTypeOptionsProvider(description, this._themeService, styleSheet, providerArgs);
167
} else {
168
provider = new DecorationSubTypeOptionsProvider(this._themeService, styleSheet, providerArgs);
169
}
170
this._decorationOptionProviders.set(key, provider);
171
this._onDecorationTypeRegistered.fire(key);
172
}
173
provider.refCount++;
174
return {
175
dispose: () => {
176
this.removeDecorationType(key);
177
}
178
};
179
}
180
181
public listDecorationTypes(): string[] {
182
return Array.from(this._decorationOptionProviders.keys());
183
}
184
185
public removeDecorationType(key: string): void {
186
const provider = this._decorationOptionProviders.get(key);
187
if (provider) {
188
provider.refCount--;
189
if (provider.refCount <= 0) {
190
this._decorationOptionProviders.delete(key);
191
provider.dispose();
192
this.listCodeEditors().forEach((ed) => ed.removeDecorationsByType(key));
193
}
194
}
195
}
196
197
public resolveDecorationOptions(decorationTypeKey: string, writable: boolean): IModelDecorationOptions {
198
const provider = this._decorationOptionProviders.get(decorationTypeKey);
199
if (!provider) {
200
throw new Error('Unknown decoration type key: ' + decorationTypeKey);
201
}
202
return provider.getOptions(this, writable);
203
}
204
205
public resolveDecorationCSSRules(decorationTypeKey: string) {
206
const provider = this._decorationOptionProviders.get(decorationTypeKey);
207
if (!provider) {
208
return null;
209
}
210
return provider.resolveDecorationCSSRules();
211
}
212
213
private readonly _transientWatchers = this._register(new DisposableMap<string, ModelTransientSettingWatcher>());
214
private readonly _modelProperties = new Map<string, Map<string, any>>();
215
216
public setModelProperty(resource: URI, key: string, value: any): void {
217
const key1 = resource.toString();
218
let dest: Map<string, any>;
219
if (this._modelProperties.has(key1)) {
220
dest = this._modelProperties.get(key1)!;
221
} else {
222
dest = new Map<string, any>();
223
this._modelProperties.set(key1, dest);
224
}
225
226
dest.set(key, value);
227
}
228
229
public getModelProperty(resource: URI, key: string): any {
230
const key1 = resource.toString();
231
if (this._modelProperties.has(key1)) {
232
const innerMap = this._modelProperties.get(key1)!;
233
return innerMap.get(key);
234
}
235
return undefined;
236
}
237
238
public setTransientModelProperty(model: ITextModel, key: string, value: any): void {
239
const uri = model.uri.toString();
240
241
let w = this._transientWatchers.get(uri);
242
if (!w) {
243
w = new ModelTransientSettingWatcher(uri, model, this);
244
this._transientWatchers.set(uri, w);
245
}
246
247
const previousValue = w.get(key);
248
if (previousValue !== value) {
249
w.set(key, value);
250
this._onDidChangeTransientModelProperty.fire(model);
251
}
252
}
253
254
public getTransientModelProperty(model: ITextModel, key: string): any {
255
const uri = model.uri.toString();
256
257
const watcher = this._transientWatchers.get(uri);
258
if (!watcher) {
259
return undefined;
260
}
261
262
return watcher.get(key);
263
}
264
265
public getTransientModelProperties(model: ITextModel): [string, any][] | undefined {
266
const uri = model.uri.toString();
267
268
const watcher = this._transientWatchers.get(uri);
269
if (!watcher) {
270
return undefined;
271
}
272
273
return watcher.keys().map(key => [key, watcher.get(key)]);
274
}
275
276
_removeWatcher(w: ModelTransientSettingWatcher): void {
277
this._transientWatchers.deleteAndDispose(w.uri);
278
}
279
280
abstract getActiveCodeEditor(): ICodeEditor | null;
281
282
async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
283
for (const handler of this._codeEditorOpenHandlers) {
284
const candidate = await handler(input, source, sideBySide);
285
if (candidate !== null) {
286
return candidate;
287
}
288
}
289
return null;
290
}
291
292
registerCodeEditorOpenHandler(handler: ICodeEditorOpenHandler): IDisposable {
293
const rm = this._codeEditorOpenHandlers.unshift(handler);
294
return toDisposable(rm);
295
}
296
}
297
298
export class ModelTransientSettingWatcher extends Disposable {
299
public readonly uri: string;
300
private readonly _values: { [key: string]: any };
301
302
constructor(uri: string, model: ITextModel, owner: AbstractCodeEditorService) {
303
super();
304
305
this.uri = uri;
306
this._values = {};
307
this._register(model.onWillDispose(() => owner._removeWatcher(this)));
308
}
309
310
public set(key: string, value: any): void {
311
this._values[key] = value;
312
}
313
314
public get(key: string): any {
315
return this._values[key];
316
}
317
318
public keys(): string[] {
319
return Object.keys(this._values);
320
}
321
}
322
323
class RefCountedStyleSheet {
324
325
private readonly _parent: AbstractCodeEditorService;
326
private readonly _editorId: string;
327
private readonly _styleSheet: HTMLStyleElement;
328
private _refCount: number;
329
330
public get sheet() {
331
return this._styleSheet.sheet as CSSStyleSheet;
332
}
333
334
constructor(parent: AbstractCodeEditorService, editorId: string, styleSheet: HTMLStyleElement) {
335
this._parent = parent;
336
this._editorId = editorId;
337
this._styleSheet = styleSheet;
338
this._refCount = 0;
339
}
340
341
public ref(): void {
342
this._refCount++;
343
}
344
345
public unref(): void {
346
this._refCount--;
347
if (this._refCount === 0) {
348
this._styleSheet.remove();
349
this._parent._removeEditorStyleSheets(this._editorId);
350
}
351
}
352
353
public insertRule(selector: string, rule: string): void {
354
domStylesheets.createCSSRule(selector, rule, this._styleSheet);
355
}
356
357
public removeRulesContainingSelector(ruleName: string): void {
358
domStylesheets.removeCSSRulesContainingSelector(ruleName, this._styleSheet);
359
}
360
}
361
362
export class GlobalStyleSheet {
363
private readonly _styleSheet: HTMLStyleElement;
364
365
public get sheet() {
366
return this._styleSheet.sheet as CSSStyleSheet;
367
}
368
369
constructor(styleSheet: HTMLStyleElement) {
370
this._styleSheet = styleSheet;
371
}
372
373
public ref(): void {
374
}
375
376
public unref(): void {
377
}
378
379
public insertRule(selector: string, rule: string): void {
380
domStylesheets.createCSSRule(selector, rule, this._styleSheet);
381
}
382
383
public removeRulesContainingSelector(ruleName: string): void {
384
domStylesheets.removeCSSRulesContainingSelector(ruleName, this._styleSheet);
385
}
386
}
387
388
interface IModelDecorationOptionsProvider extends IDisposable {
389
refCount: number;
390
getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions;
391
resolveDecorationCSSRules(): CSSRuleList;
392
}
393
394
class DecorationSubTypeOptionsProvider implements IModelDecorationOptionsProvider {
395
396
private readonly _styleSheet: GlobalStyleSheet | RefCountedStyleSheet;
397
public refCount: number;
398
399
private readonly _parentTypeKey: string;
400
private _beforeContentRules: DecorationCSSRules | null;
401
private _afterContentRules: DecorationCSSRules | null;
402
403
constructor(themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) {
404
this._styleSheet = styleSheet;
405
this._styleSheet.ref();
406
this._parentTypeKey = providerArgs.parentTypeKey!;
407
this.refCount = 0;
408
409
this._beforeContentRules = new DecorationCSSRules(ModelDecorationCSSRuleType.BeforeContentClassName, providerArgs, themeService);
410
this._afterContentRules = new DecorationCSSRules(ModelDecorationCSSRuleType.AfterContentClassName, providerArgs, themeService);
411
}
412
413
public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions {
414
const options = codeEditorService.resolveDecorationOptions(this._parentTypeKey, true);
415
if (this._beforeContentRules) {
416
options.beforeContentClassName = this._beforeContentRules.className;
417
}
418
if (this._afterContentRules) {
419
options.afterContentClassName = this._afterContentRules.className;
420
}
421
return options;
422
}
423
424
public resolveDecorationCSSRules(): CSSRuleList {
425
return this._styleSheet.sheet.cssRules;
426
}
427
428
public dispose(): void {
429
if (this._beforeContentRules) {
430
this._beforeContentRules.dispose();
431
this._beforeContentRules = null;
432
}
433
if (this._afterContentRules) {
434
this._afterContentRules.dispose();
435
this._afterContentRules = null;
436
}
437
this._styleSheet.unref();
438
}
439
}
440
441
interface ProviderArguments {
442
styleSheet: GlobalStyleSheet | RefCountedStyleSheet;
443
key: string;
444
parentTypeKey?: string;
445
options: IDecorationRenderOptions;
446
}
447
448
449
class DecorationTypeOptionsProvider implements IModelDecorationOptionsProvider {
450
451
private readonly _disposables = new DisposableStore();
452
private readonly _styleSheet: GlobalStyleSheet | RefCountedStyleSheet;
453
public refCount: number;
454
455
public description: string;
456
public className: string | undefined;
457
public inlineClassName: string | undefined;
458
public inlineClassNameAffectsLetterSpacing: boolean | undefined;
459
public beforeContentClassName: string | undefined;
460
public afterContentClassName: string | undefined;
461
public glyphMarginClassName: string | undefined;
462
public isWholeLine: boolean;
463
public lineHeight: number | undefined;
464
public fontSize: string | undefined;
465
public fontFamily: string | undefined;
466
public fontWeight: string | undefined;
467
public fontStyle: string | undefined;
468
public overviewRuler: IModelDecorationOverviewRulerOptions | undefined;
469
public stickiness: TrackedRangeStickiness | undefined;
470
public beforeInjectedText: InjectedTextOptions | undefined;
471
public afterInjectedText: InjectedTextOptions | undefined;
472
473
constructor(description: string, themeService: IThemeService, styleSheet: GlobalStyleSheet | RefCountedStyleSheet, providerArgs: ProviderArguments) {
474
this.description = description;
475
476
this._styleSheet = styleSheet;
477
this._styleSheet.ref();
478
this.refCount = 0;
479
480
const createCSSRules = (type: ModelDecorationCSSRuleType) => {
481
const rules = new DecorationCSSRules(type, providerArgs, themeService);
482
this._disposables.add(rules);
483
if (rules.hasContent) {
484
return rules.className;
485
}
486
return undefined;
487
};
488
const createInlineCSSRules = (type: ModelDecorationCSSRuleType) => {
489
const rules = new DecorationCSSRules(type, providerArgs, themeService);
490
this._disposables.add(rules);
491
if (rules.hasContent) {
492
return { className: rules.className, hasLetterSpacing: rules.hasLetterSpacing };
493
}
494
return null;
495
};
496
497
this.className = createCSSRules(ModelDecorationCSSRuleType.ClassName);
498
const inlineData = createInlineCSSRules(ModelDecorationCSSRuleType.InlineClassName);
499
if (inlineData) {
500
this.inlineClassName = inlineData.className;
501
this.inlineClassNameAffectsLetterSpacing = inlineData.hasLetterSpacing;
502
}
503
this.beforeContentClassName = createCSSRules(ModelDecorationCSSRuleType.BeforeContentClassName);
504
this.afterContentClassName = createCSSRules(ModelDecorationCSSRuleType.AfterContentClassName);
505
506
if (providerArgs.options.beforeInjectedText && providerArgs.options.beforeInjectedText.contentText) {
507
const beforeInlineData = createInlineCSSRules(ModelDecorationCSSRuleType.BeforeInjectedTextClassName);
508
this.beforeInjectedText = {
509
content: providerArgs.options.beforeInjectedText.contentText,
510
inlineClassName: beforeInlineData?.className,
511
inlineClassNameAffectsLetterSpacing: beforeInlineData?.hasLetterSpacing || providerArgs.options.beforeInjectedText.affectsLetterSpacing
512
};
513
}
514
515
if (providerArgs.options.afterInjectedText && providerArgs.options.afterInjectedText.contentText) {
516
const afterInlineData = createInlineCSSRules(ModelDecorationCSSRuleType.AfterInjectedTextClassName);
517
this.afterInjectedText = {
518
content: providerArgs.options.afterInjectedText.contentText,
519
inlineClassName: afterInlineData?.className,
520
inlineClassNameAffectsLetterSpacing: afterInlineData?.hasLetterSpacing || providerArgs.options.afterInjectedText.affectsLetterSpacing
521
};
522
}
523
524
this.glyphMarginClassName = createCSSRules(ModelDecorationCSSRuleType.GlyphMarginClassName);
525
526
const options = providerArgs.options;
527
this.isWholeLine = Boolean(options.isWholeLine);
528
this.lineHeight = options.lineHeight;
529
this.fontFamily = options.fontFamily;
530
this.fontSize = options.fontSize;
531
this.fontWeight = options.fontWeight;
532
this.fontStyle = options.fontStyle;
533
this.stickiness = options.rangeBehavior;
534
535
const lightOverviewRulerColor = options.light && options.light.overviewRulerColor || options.overviewRulerColor;
536
const darkOverviewRulerColor = options.dark && options.dark.overviewRulerColor || options.overviewRulerColor;
537
if (
538
typeof lightOverviewRulerColor !== 'undefined'
539
|| typeof darkOverviewRulerColor !== 'undefined'
540
) {
541
this.overviewRuler = {
542
color: lightOverviewRulerColor || darkOverviewRulerColor,
543
darkColor: darkOverviewRulerColor || lightOverviewRulerColor,
544
position: options.overviewRulerLane || OverviewRulerLane.Center
545
};
546
}
547
}
548
549
public getOptions(codeEditorService: AbstractCodeEditorService, writable: boolean): IModelDecorationOptions {
550
if (!writable) {
551
return this;
552
}
553
554
return {
555
description: this.description,
556
inlineClassName: this.inlineClassName,
557
beforeContentClassName: this.beforeContentClassName,
558
afterContentClassName: this.afterContentClassName,
559
className: this.className,
560
glyphMarginClassName: this.glyphMarginClassName,
561
isWholeLine: this.isWholeLine,
562
lineHeight: this.lineHeight,
563
fontFamily: this.fontFamily,
564
fontSize: this.fontSize,
565
fontWeight: this.fontWeight,
566
fontStyle: this.fontStyle,
567
overviewRuler: this.overviewRuler,
568
stickiness: this.stickiness,
569
before: this.beforeInjectedText,
570
after: this.afterInjectedText
571
};
572
}
573
574
public resolveDecorationCSSRules(): CSSRuleList {
575
return this._styleSheet.sheet.rules;
576
}
577
578
public dispose(): void {
579
this._disposables.dispose();
580
this._styleSheet.unref();
581
}
582
}
583
584
585
export const _CSS_MAP: { [prop: string]: string } = {
586
color: 'color:{0} !important;',
587
opacity: 'opacity:{0};',
588
backgroundColor: 'background-color:{0};',
589
590
outline: 'outline:{0};',
591
outlineColor: 'outline-color:{0};',
592
outlineStyle: 'outline-style:{0};',
593
outlineWidth: 'outline-width:{0};',
594
595
border: 'border:{0};',
596
borderColor: 'border-color:{0};',
597
borderRadius: 'border-radius:{0};',
598
borderSpacing: 'border-spacing:{0};',
599
borderStyle: 'border-style:{0};',
600
borderWidth: 'border-width:{0};',
601
602
fontStyle: 'font-style:{0};',
603
fontWeight: 'font-weight:{0};',
604
fontSize: 'font-size:{0};',
605
fontFamily: 'font-family:{0};',
606
textDecoration: 'text-decoration:{0};',
607
cursor: 'cursor:{0};',
608
letterSpacing: 'letter-spacing:{0};',
609
610
gutterIconPath: 'background:{0} center center no-repeat;',
611
gutterIconSize: 'background-size:{0};',
612
613
contentText: 'content:\'{0}\';',
614
contentIconPath: 'content:{0};',
615
margin: 'margin:{0};',
616
padding: 'padding:{0};',
617
width: 'width:{0};',
618
height: 'height:{0};',
619
620
verticalAlign: 'vertical-align:{0};',
621
};
622
623
624
class DecorationCSSRules {
625
626
private _theme: IColorTheme;
627
private readonly _className: string;
628
private readonly _unThemedSelector: string;
629
private _hasContent: boolean;
630
private _hasLetterSpacing: boolean;
631
private readonly _ruleType: ModelDecorationCSSRuleType;
632
private _themeListener: IDisposable | null;
633
private readonly _providerArgs: ProviderArguments;
634
private _usesThemeColors: boolean;
635
636
constructor(ruleType: ModelDecorationCSSRuleType, providerArgs: ProviderArguments, themeService: IThemeService) {
637
this._theme = themeService.getColorTheme();
638
this._ruleType = ruleType;
639
this._providerArgs = providerArgs;
640
this._usesThemeColors = false;
641
this._hasContent = false;
642
this._hasLetterSpacing = false;
643
644
let className = CSSNameHelper.getClassName(this._providerArgs.key, ruleType);
645
if (this._providerArgs.parentTypeKey) {
646
className = className + ' ' + CSSNameHelper.getClassName(this._providerArgs.parentTypeKey, ruleType);
647
}
648
this._className = className;
649
650
this._unThemedSelector = CSSNameHelper.getSelector(this._providerArgs.key, this._providerArgs.parentTypeKey, ruleType);
651
652
this._buildCSS();
653
654
if (this._usesThemeColors) {
655
this._themeListener = themeService.onDidColorThemeChange(theme => {
656
this._theme = themeService.getColorTheme();
657
this._removeCSS();
658
this._buildCSS();
659
});
660
} else {
661
this._themeListener = null;
662
}
663
}
664
665
public dispose() {
666
if (this._hasContent) {
667
this._removeCSS();
668
this._hasContent = false;
669
}
670
if (this._themeListener) {
671
this._themeListener.dispose();
672
this._themeListener = null;
673
}
674
}
675
676
public get hasContent(): boolean {
677
return this._hasContent;
678
}
679
680
public get hasLetterSpacing(): boolean {
681
return this._hasLetterSpacing;
682
}
683
684
public get className(): string {
685
return this._className;
686
}
687
688
private _buildCSS(): void {
689
const options = this._providerArgs.options;
690
let unthemedCSS: string, lightCSS: string, darkCSS: string;
691
switch (this._ruleType) {
692
case ModelDecorationCSSRuleType.ClassName:
693
unthemedCSS = this.getCSSTextForModelDecorationClassName(options);
694
lightCSS = this.getCSSTextForModelDecorationClassName(options.light);
695
darkCSS = this.getCSSTextForModelDecorationClassName(options.dark);
696
break;
697
case ModelDecorationCSSRuleType.InlineClassName:
698
unthemedCSS = this.getCSSTextForModelDecorationInlineClassName(options);
699
lightCSS = this.getCSSTextForModelDecorationInlineClassName(options.light);
700
darkCSS = this.getCSSTextForModelDecorationInlineClassName(options.dark);
701
break;
702
case ModelDecorationCSSRuleType.GlyphMarginClassName:
703
unthemedCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options);
704
lightCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options.light);
705
darkCSS = this.getCSSTextForModelDecorationGlyphMarginClassName(options.dark);
706
break;
707
case ModelDecorationCSSRuleType.BeforeContentClassName:
708
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.before);
709
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.before);
710
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.before);
711
break;
712
case ModelDecorationCSSRuleType.AfterContentClassName:
713
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.after);
714
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.after);
715
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.after);
716
break;
717
case ModelDecorationCSSRuleType.BeforeInjectedTextClassName:
718
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.beforeInjectedText);
719
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.beforeInjectedText);
720
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.beforeInjectedText);
721
break;
722
case ModelDecorationCSSRuleType.AfterInjectedTextClassName:
723
unthemedCSS = this.getCSSTextForModelDecorationContentClassName(options.afterInjectedText);
724
lightCSS = this.getCSSTextForModelDecorationContentClassName(options.light && options.light.afterInjectedText);
725
darkCSS = this.getCSSTextForModelDecorationContentClassName(options.dark && options.dark.afterInjectedText);
726
break;
727
default:
728
throw new Error('Unknown rule type: ' + this._ruleType);
729
}
730
const sheet = this._providerArgs.styleSheet;
731
732
let hasContent = false;
733
if (unthemedCSS.length > 0) {
734
sheet.insertRule(this._unThemedSelector, unthemedCSS);
735
hasContent = true;
736
}
737
if (lightCSS.length > 0) {
738
sheet.insertRule(`.vs${this._unThemedSelector}, .hc-light${this._unThemedSelector}`, lightCSS);
739
hasContent = true;
740
}
741
if (darkCSS.length > 0) {
742
sheet.insertRule(`.vs-dark${this._unThemedSelector}, .hc-black${this._unThemedSelector}`, darkCSS);
743
hasContent = true;
744
}
745
this._hasContent = hasContent;
746
}
747
748
private _removeCSS(): void {
749
this._providerArgs.styleSheet.removeRulesContainingSelector(this._unThemedSelector);
750
}
751
752
/**
753
* Build the CSS for decorations styled via `className`.
754
*/
755
private getCSSTextForModelDecorationClassName(opts: IThemeDecorationRenderOptions | undefined): string {
756
if (!opts) {
757
return '';
758
}
759
const cssTextArr: string[] = [];
760
this.collectCSSText(opts, ['backgroundColor'], cssTextArr);
761
this.collectCSSText(opts, ['outline', 'outlineColor', 'outlineStyle', 'outlineWidth'], cssTextArr);
762
this.collectBorderSettingsCSSText(opts, cssTextArr);
763
return cssTextArr.join('');
764
}
765
766
/**
767
* Build the CSS for decorations styled via `inlineClassName`.
768
*/
769
private getCSSTextForModelDecorationInlineClassName(opts: IThemeDecorationRenderOptions | undefined): string {
770
if (!opts) {
771
return '';
772
}
773
const cssTextArr: string[] = [];
774
this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'fontFamily', 'fontSize', 'textDecoration', 'cursor', 'color', 'opacity', 'letterSpacing'], cssTextArr);
775
if (opts.letterSpacing) {
776
this._hasLetterSpacing = true;
777
}
778
return cssTextArr.join('');
779
}
780
781
/**
782
* Build the CSS for decorations styled before or after content.
783
*/
784
private getCSSTextForModelDecorationContentClassName(opts: IContentDecorationRenderOptions | undefined): string {
785
if (!opts) {
786
return '';
787
}
788
const cssTextArr: string[] = [];
789
790
if (typeof opts !== 'undefined') {
791
this.collectBorderSettingsCSSText(opts, cssTextArr);
792
if (typeof opts.contentIconPath !== 'undefined') {
793
cssTextArr.push(strings.format(_CSS_MAP.contentIconPath, cssJs.asCSSUrl(URI.revive(opts.contentIconPath))));
794
}
795
if (typeof opts.contentText === 'string') {
796
const truncated = opts.contentText.match(/^.*$/m)![0]; // only take first line
797
const escaped = truncated.replace(/['\\]/g, '\\$&');
798
799
cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped));
800
}
801
this.collectCSSText(opts, ['verticalAlign', 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin', 'padding'], cssTextArr);
802
if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) {
803
cssTextArr.push('display:inline-block;');
804
}
805
}
806
807
return cssTextArr.join('');
808
}
809
810
/**
811
* Build the CSS for decorations styled via `glyphMarginClassName`.
812
*/
813
private getCSSTextForModelDecorationGlyphMarginClassName(opts: IThemeDecorationRenderOptions | undefined): string {
814
if (!opts) {
815
return '';
816
}
817
const cssTextArr: string[] = [];
818
819
if (typeof opts.gutterIconPath !== 'undefined') {
820
cssTextArr.push(strings.format(_CSS_MAP.gutterIconPath, cssJs.asCSSUrl(URI.revive(opts.gutterIconPath))));
821
if (typeof opts.gutterIconSize !== 'undefined') {
822
cssTextArr.push(strings.format(_CSS_MAP.gutterIconSize, opts.gutterIconSize));
823
}
824
}
825
826
return cssTextArr.join('');
827
}
828
829
private collectBorderSettingsCSSText(opts: any, cssTextArr: string[]): boolean {
830
if (this.collectCSSText(opts, ['border', 'borderColor', 'borderRadius', 'borderSpacing', 'borderStyle', 'borderWidth'], cssTextArr)) {
831
cssTextArr.push(strings.format('box-sizing: border-box;'));
832
return true;
833
}
834
return false;
835
}
836
837
private collectCSSText(opts: any, properties: string[], cssTextArr: string[]): boolean {
838
const lenBefore = cssTextArr.length;
839
for (const property of properties) {
840
const value = this.resolveValue(opts[property]);
841
if (typeof value === 'string') {
842
cssTextArr.push(strings.format(_CSS_MAP[property], value));
843
}
844
}
845
return cssTextArr.length !== lenBefore;
846
}
847
848
private resolveValue(value: string | ThemeColor): string {
849
if (isThemeColor(value)) {
850
this._usesThemeColors = true;
851
const color = this._theme.getColor(value.id);
852
if (color) {
853
return color.toString();
854
}
855
return 'transparent';
856
}
857
return value;
858
}
859
}
860
861
const enum ModelDecorationCSSRuleType {
862
ClassName = 0,
863
InlineClassName = 1,
864
GlyphMarginClassName = 2,
865
BeforeContentClassName = 3,
866
AfterContentClassName = 4,
867
BeforeInjectedTextClassName = 5,
868
AfterInjectedTextClassName = 6,
869
}
870
871
class CSSNameHelper {
872
873
public static getClassName(key: string, type: ModelDecorationCSSRuleType): string {
874
return 'ced-' + key + '-' + type;
875
}
876
877
public static getSelector(key: string, parentKey: string | undefined, ruleType: ModelDecorationCSSRuleType): string {
878
let selector = '.monaco-editor .' + this.getClassName(key, ruleType);
879
if (parentKey) {
880
selector = selector + '.' + this.getClassName(parentKey, ruleType);
881
}
882
if (ruleType === ModelDecorationCSSRuleType.BeforeContentClassName) {
883
selector += '::before';
884
} else if (ruleType === ModelDecorationCSSRuleType.AfterContentClassName) {
885
selector += '::after';
886
}
887
return selector;
888
}
889
}
890
891