Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/languages/languageConfigurationRegistry.ts
3296 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 { Emitter, Event } from '../../../base/common/event.js';
7
import { Disposable, IDisposable, markAsSingleton, toDisposable } from '../../../base/common/lifecycle.js';
8
import * as strings from '../../../base/common/strings.js';
9
import { ITextModel } from '../model.js';
10
import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from '../core/wordHelper.js';
11
import { EnterAction, FoldingRules, IAutoClosingPair, IndentationRule, LanguageConfiguration, AutoClosingPairs, CharacterPair, ExplicitLanguageConfiguration } from './languageConfiguration.js';
12
import { CharacterPairSupport } from './supports/characterPair.js';
13
import { BracketElectricCharacterSupport } from './supports/electricCharacter.js';
14
import { IndentRulesSupport } from './supports/indentRules.js';
15
import { OnEnterSupport } from './supports/onEnter.js';
16
import { RichEditBrackets } from './supports/richEditBrackets.js';
17
import { EditorAutoIndentStrategy } from '../config/editorOptions.js';
18
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
19
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
20
import { ILanguageService } from './language.js';
21
import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';
22
import { PLAINTEXT_LANGUAGE_ID } from './modesRegistry.js';
23
import { LanguageBracketsConfiguration } from './supports/languageBracketsConfiguration.js';
24
25
/**
26
* Interface used to support insertion of mode specific comments.
27
*/
28
export interface ICommentsConfiguration {
29
lineCommentToken?: string;
30
lineCommentNoIndent?: boolean;
31
blockCommentStartToken?: string;
32
blockCommentEndToken?: string;
33
}
34
35
export interface ILanguageConfigurationService {
36
readonly _serviceBrand: undefined;
37
38
onDidChange: Event<LanguageConfigurationServiceChangeEvent>;
39
40
/**
41
* @param priority Use a higher number for higher priority
42
*/
43
register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable;
44
45
getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration;
46
47
}
48
49
export class LanguageConfigurationServiceChangeEvent {
50
constructor(public readonly languageId: string | undefined) { }
51
52
public affects(languageId: string): boolean {
53
return !this.languageId ? true : this.languageId === languageId;
54
}
55
}
56
57
export const ILanguageConfigurationService = createDecorator<ILanguageConfigurationService>('languageConfigurationService');
58
59
export class LanguageConfigurationService extends Disposable implements ILanguageConfigurationService {
60
_serviceBrand: undefined;
61
62
private readonly _registry = this._register(new LanguageConfigurationRegistry());
63
64
private readonly onDidChangeEmitter = this._register(new Emitter<LanguageConfigurationServiceChangeEvent>());
65
public readonly onDidChange = this.onDidChangeEmitter.event;
66
67
private readonly configurations = new Map<string, ResolvedLanguageConfiguration>();
68
69
constructor(
70
@IConfigurationService private readonly configurationService: IConfigurationService,
71
@ILanguageService private readonly languageService: ILanguageService
72
) {
73
super();
74
75
const languageConfigKeys = new Set(Object.values(customizedLanguageConfigKeys));
76
77
this._register(this.configurationService.onDidChangeConfiguration((e) => {
78
const globalConfigChanged = e.change.keys.some((k) =>
79
languageConfigKeys.has(k)
80
);
81
const localConfigChanged = e.change.overrides
82
.filter(([overrideLangName, keys]) =>
83
keys.some((k) => languageConfigKeys.has(k))
84
)
85
.map(([overrideLangName]) => overrideLangName);
86
87
if (globalConfigChanged) {
88
this.configurations.clear();
89
this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(undefined));
90
} else {
91
for (const languageId of localConfigChanged) {
92
if (this.languageService.isRegisteredLanguageId(languageId)) {
93
this.configurations.delete(languageId);
94
this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(languageId));
95
}
96
}
97
}
98
}));
99
100
this._register(this._registry.onDidChange((e) => {
101
this.configurations.delete(e.languageId);
102
this.onDidChangeEmitter.fire(new LanguageConfigurationServiceChangeEvent(e.languageId));
103
}));
104
}
105
106
public register(languageId: string, configuration: LanguageConfiguration, priority?: number): IDisposable {
107
return this._registry.register(languageId, configuration, priority);
108
}
109
110
public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration {
111
let result = this.configurations.get(languageId);
112
if (!result) {
113
result = computeConfig(languageId, this._registry, this.configurationService, this.languageService);
114
this.configurations.set(languageId, result);
115
}
116
return result;
117
}
118
}
119
120
function computeConfig(
121
languageId: string,
122
registry: LanguageConfigurationRegistry,
123
configurationService: IConfigurationService,
124
languageService: ILanguageService,
125
): ResolvedLanguageConfiguration {
126
let languageConfig = registry.getLanguageConfiguration(languageId);
127
128
if (!languageConfig) {
129
if (!languageService.isRegisteredLanguageId(languageId)) {
130
// this happens for the null language, which can be returned by monarch.
131
// Instead of throwing an error, we just return a default config.
132
return new ResolvedLanguageConfiguration(languageId, {});
133
}
134
languageConfig = new ResolvedLanguageConfiguration(languageId, {});
135
}
136
137
const customizedConfig = getCustomizedLanguageConfig(languageConfig.languageId, configurationService);
138
const data = combineLanguageConfigurations([languageConfig.underlyingConfig, customizedConfig]);
139
const config = new ResolvedLanguageConfiguration(languageConfig.languageId, data);
140
return config;
141
}
142
143
const customizedLanguageConfigKeys = {
144
brackets: 'editor.language.brackets',
145
colorizedBracketPairs: 'editor.language.colorizedBracketPairs'
146
};
147
148
function getCustomizedLanguageConfig(languageId: string, configurationService: IConfigurationService): LanguageConfiguration {
149
const brackets = configurationService.getValue(customizedLanguageConfigKeys.brackets, {
150
overrideIdentifier: languageId,
151
});
152
153
const colorizedBracketPairs = configurationService.getValue(customizedLanguageConfigKeys.colorizedBracketPairs, {
154
overrideIdentifier: languageId,
155
});
156
157
return {
158
brackets: validateBracketPairs(brackets),
159
colorizedBracketPairs: validateBracketPairs(colorizedBracketPairs),
160
};
161
}
162
163
function validateBracketPairs(data: unknown): CharacterPair[] | undefined {
164
if (!Array.isArray(data)) {
165
return undefined;
166
}
167
return data.map(pair => {
168
if (!Array.isArray(pair) || pair.length !== 2) {
169
return undefined;
170
}
171
return [pair[0], pair[1]] as CharacterPair;
172
}).filter((p): p is CharacterPair => !!p);
173
}
174
175
export function getIndentationAtPosition(model: ITextModel, lineNumber: number, column: number): string {
176
const lineText = model.getLineContent(lineNumber);
177
let indentation = strings.getLeadingWhitespace(lineText);
178
if (indentation.length > column - 1) {
179
indentation = indentation.substring(0, column - 1);
180
}
181
return indentation;
182
}
183
184
class ComposedLanguageConfiguration {
185
private readonly _entries: LanguageConfigurationContribution[];
186
private _order: number;
187
private _resolved: ResolvedLanguageConfiguration | null = null;
188
189
constructor(public readonly languageId: string) {
190
this._entries = [];
191
this._order = 0;
192
this._resolved = null;
193
}
194
195
public register(
196
configuration: LanguageConfiguration,
197
priority: number
198
): IDisposable {
199
const entry = new LanguageConfigurationContribution(
200
configuration,
201
priority,
202
++this._order
203
);
204
this._entries.push(entry);
205
this._resolved = null;
206
return markAsSingleton(toDisposable(() => {
207
for (let i = 0; i < this._entries.length; i++) {
208
if (this._entries[i] === entry) {
209
this._entries.splice(i, 1);
210
this._resolved = null;
211
break;
212
}
213
}
214
}));
215
}
216
217
public getResolvedConfiguration(): ResolvedLanguageConfiguration | null {
218
if (!this._resolved) {
219
const config = this._resolve();
220
if (config) {
221
this._resolved = new ResolvedLanguageConfiguration(
222
this.languageId,
223
config
224
);
225
}
226
}
227
return this._resolved;
228
}
229
230
private _resolve(): LanguageConfiguration | null {
231
if (this._entries.length === 0) {
232
return null;
233
}
234
this._entries.sort(LanguageConfigurationContribution.cmp);
235
return combineLanguageConfigurations(this._entries.map(e => e.configuration));
236
}
237
}
238
239
function combineLanguageConfigurations(configs: LanguageConfiguration[]): LanguageConfiguration {
240
let result: ExplicitLanguageConfiguration = {
241
comments: undefined,
242
brackets: undefined,
243
wordPattern: undefined,
244
indentationRules: undefined,
245
onEnterRules: undefined,
246
autoClosingPairs: undefined,
247
surroundingPairs: undefined,
248
autoCloseBefore: undefined,
249
folding: undefined,
250
colorizedBracketPairs: undefined,
251
__electricCharacterSupport: undefined,
252
};
253
for (const entry of configs) {
254
result = {
255
comments: entry.comments || result.comments,
256
brackets: entry.brackets || result.brackets,
257
wordPattern: entry.wordPattern || result.wordPattern,
258
indentationRules: entry.indentationRules || result.indentationRules,
259
onEnterRules: entry.onEnterRules || result.onEnterRules,
260
autoClosingPairs: entry.autoClosingPairs || result.autoClosingPairs,
261
surroundingPairs: entry.surroundingPairs || result.surroundingPairs,
262
autoCloseBefore: entry.autoCloseBefore || result.autoCloseBefore,
263
folding: entry.folding || result.folding,
264
colorizedBracketPairs: entry.colorizedBracketPairs || result.colorizedBracketPairs,
265
__electricCharacterSupport: entry.__electricCharacterSupport || result.__electricCharacterSupport,
266
};
267
}
268
269
return result;
270
}
271
272
class LanguageConfigurationContribution {
273
constructor(
274
public readonly configuration: LanguageConfiguration,
275
public readonly priority: number,
276
public readonly order: number
277
) { }
278
279
public static cmp(a: LanguageConfigurationContribution, b: LanguageConfigurationContribution) {
280
if (a.priority === b.priority) {
281
// higher order last
282
return a.order - b.order;
283
}
284
// higher priority last
285
return a.priority - b.priority;
286
}
287
}
288
289
export class LanguageConfigurationChangeEvent {
290
constructor(public readonly languageId: string) { }
291
}
292
293
export class LanguageConfigurationRegistry extends Disposable {
294
private readonly _entries = new Map<string, ComposedLanguageConfiguration>();
295
296
private readonly _onDidChange = this._register(new Emitter<LanguageConfigurationChangeEvent>());
297
public readonly onDidChange: Event<LanguageConfigurationChangeEvent> = this._onDidChange.event;
298
299
constructor() {
300
super();
301
this._register(this.register(PLAINTEXT_LANGUAGE_ID, {
302
brackets: [
303
['(', ')'],
304
['[', ']'],
305
['{', '}'],
306
],
307
surroundingPairs: [
308
{ open: '{', close: '}' },
309
{ open: '[', close: ']' },
310
{ open: '(', close: ')' },
311
{ open: '<', close: '>' },
312
{ open: '\"', close: '\"' },
313
{ open: '\'', close: '\'' },
314
{ open: '`', close: '`' },
315
],
316
colorizedBracketPairs: [],
317
folding: {
318
offSide: true
319
}
320
}, 0));
321
}
322
323
/**
324
* @param priority Use a higher number for higher priority
325
*/
326
public register(languageId: string, configuration: LanguageConfiguration, priority: number = 0): IDisposable {
327
let entries = this._entries.get(languageId);
328
if (!entries) {
329
entries = new ComposedLanguageConfiguration(languageId);
330
this._entries.set(languageId, entries);
331
}
332
333
const disposable = entries.register(configuration, priority);
334
this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId));
335
336
return markAsSingleton(toDisposable(() => {
337
disposable.dispose();
338
this._onDidChange.fire(new LanguageConfigurationChangeEvent(languageId));
339
}));
340
}
341
342
public getLanguageConfiguration(languageId: string): ResolvedLanguageConfiguration | null {
343
const entries = this._entries.get(languageId);
344
return entries?.getResolvedConfiguration() || null;
345
}
346
}
347
348
/**
349
* Immutable.
350
*/
351
export class ResolvedLanguageConfiguration {
352
private _brackets: RichEditBrackets | null;
353
private _electricCharacter: BracketElectricCharacterSupport | null;
354
private readonly _onEnterSupport: OnEnterSupport | null;
355
356
public readonly comments: ICommentsConfiguration | null;
357
public readonly characterPair: CharacterPairSupport;
358
public readonly wordDefinition: RegExp;
359
public readonly indentRulesSupport: IndentRulesSupport | null;
360
public readonly indentationRules: IndentationRule | undefined;
361
public readonly foldingRules: FoldingRules;
362
public readonly bracketsNew: LanguageBracketsConfiguration;
363
364
constructor(
365
public readonly languageId: string,
366
public readonly underlyingConfig: LanguageConfiguration
367
) {
368
this._brackets = null;
369
this._electricCharacter = null;
370
this._onEnterSupport =
371
this.underlyingConfig.brackets ||
372
this.underlyingConfig.indentationRules ||
373
this.underlyingConfig.onEnterRules
374
? new OnEnterSupport(this.underlyingConfig)
375
: null;
376
this.comments = ResolvedLanguageConfiguration._handleComments(this.underlyingConfig);
377
this.characterPair = new CharacterPairSupport(this.underlyingConfig);
378
379
this.wordDefinition = this.underlyingConfig.wordPattern || DEFAULT_WORD_REGEXP;
380
this.indentationRules = this.underlyingConfig.indentationRules;
381
if (this.underlyingConfig.indentationRules) {
382
this.indentRulesSupport = new IndentRulesSupport(
383
this.underlyingConfig.indentationRules
384
);
385
} else {
386
this.indentRulesSupport = null;
387
}
388
this.foldingRules = this.underlyingConfig.folding || {};
389
390
this.bracketsNew = new LanguageBracketsConfiguration(
391
languageId,
392
this.underlyingConfig
393
);
394
}
395
396
public getWordDefinition(): RegExp {
397
return ensureValidWordDefinition(this.wordDefinition);
398
}
399
400
public get brackets(): RichEditBrackets | null {
401
if (!this._brackets && this.underlyingConfig.brackets) {
402
this._brackets = new RichEditBrackets(
403
this.languageId,
404
this.underlyingConfig.brackets
405
);
406
}
407
return this._brackets;
408
}
409
410
public get electricCharacter(): BracketElectricCharacterSupport | null {
411
if (!this._electricCharacter) {
412
this._electricCharacter = new BracketElectricCharacterSupport(
413
this.brackets
414
);
415
}
416
return this._electricCharacter;
417
}
418
419
public onEnter(
420
autoIndent: EditorAutoIndentStrategy,
421
previousLineText: string,
422
beforeEnterText: string,
423
afterEnterText: string
424
): EnterAction | null {
425
if (!this._onEnterSupport) {
426
return null;
427
}
428
return this._onEnterSupport.onEnter(
429
autoIndent,
430
previousLineText,
431
beforeEnterText,
432
afterEnterText
433
);
434
}
435
436
public getAutoClosingPairs(): AutoClosingPairs {
437
return new AutoClosingPairs(this.characterPair.getAutoClosingPairs());
438
}
439
440
public getAutoCloseBeforeSet(forQuotes: boolean): string {
441
return this.characterPair.getAutoCloseBeforeSet(forQuotes);
442
}
443
444
public getSurroundingPairs(): IAutoClosingPair[] {
445
return this.characterPair.getSurroundingPairs();
446
}
447
448
private static _handleComments(
449
conf: LanguageConfiguration
450
): ICommentsConfiguration | null {
451
const commentRule = conf.comments;
452
if (!commentRule) {
453
return null;
454
}
455
456
// comment configuration
457
const comments: ICommentsConfiguration = {};
458
459
if (commentRule.lineComment) {
460
if (typeof commentRule.lineComment === 'string') {
461
comments.lineCommentToken = commentRule.lineComment;
462
} else {
463
comments.lineCommentToken = commentRule.lineComment.comment;
464
comments.lineCommentNoIndent = commentRule.lineComment.noIndent;
465
}
466
}
467
if (commentRule.blockComment) {
468
const [blockStart, blockEnd] = commentRule.blockComment;
469
comments.blockCommentStartToken = blockStart;
470
comments.blockCommentEndToken = blockEnd;
471
}
472
473
return comments;
474
}
475
}
476
477
registerSingleton(ILanguageConfigurationService, LanguageConfigurationService, InstantiationType.Delayed);
478
479