src/vs/editor/common/languageFeatureRegistry.ts

236 LOC · 67 covered · 169 uncovered · 14 ranges · 1341 concepts · 1 introducers · 662 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- testThemeService.ts ×18
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 } from '../../base/common/event.js';
7 > import { IDisposable, toDisposable } from '../../base/common/lifecycle.js';
8 > import { ITextModel, shouldSynchronizeModel } from './model.js';
9 > import { LanguageFilter, LanguageSelector, score, selectLanguageIds } from './languageSelector.js';
10 > import { URI } from '../../base/common/uri.js';
11 >
12 > interface Entry<T> {
13 > readonly selector: LanguageSelector;
14 > readonly provider: T;
15 > _score: number;
16 > readonly _time: number;
17 > }
18 >
19 function isExclusive(selector: LanguageSelector): boolean {
20 if (typeof selector === 'string') {
21 return false;
22 } else if (Array.isArray(selector)) {
23 return selector.every(isExclusive);
24 } else {
25 return !!(selector as LanguageFilter).exclusive; // TODO: microsoft/TypeScript#42768
26 }
27 }
29 > export interface NotebookInfo {
30 > readonly uri: URI;
31 > readonly type: string;
32 > }
33 >
34 > export interface NotebookInfoResolver {
35 > (uri: URI): NotebookInfo | undefined;
36 > }
37 >
38 > class MatchCandidate {
39 > constructor(
40 readonly uri: URI,
41 readonly languageId: string,
42 readonly notebookUri: URI | undefined,
43 readonly notebookType: string | undefined,
44 readonly recursive: boolean,
45 ) { }
47 > equals(other: MatchCandidate): boolean {
48 return this.notebookType === other.notebookType
49 && this.languageId === other.languageId
50 && this.uri.toString() === other.uri.toString()
51 && this.notebookUri?.toString() === other.notebookUri?.toString()
52 && this.recursive === other.recursive;
53 }
55 >
56 > export class LanguageFeatureRegistry<T> {
57 >
58 > private _clock: number = 0;
59 > private readonly _entries: Entry<T>[] = [];
60 >
61 > private readonly _onDidChange = new Emitter<number>();
62 > get onDidChange() { return this._onDidChange.event; }
63 >
64 > constructor(private readonly _notebookInfoResolver?: NotebookInfoResolver) { }
65 >
66 > register(selector: LanguageSelector, provider: T): IDisposable {
67
68 let entry: Entry<T> | undefined = {
69 selector,
70 provider,
71 _score: -1,
72 _time: this._clock++
73 };
74
75 this._entries.push(entry);
76 this._lastCandidate = undefined;
77 this._onDidChange.fire(this._entries.length);
78
79 return toDisposable(() => {
80 if (entry) {
81 const idx = this._entries.indexOf(entry);
82 if (idx >= 0) {
83 this._entries.splice(idx, 1);
84 this._lastCandidate = undefined;
85 this._onDidChange.fire(this._entries.length);
86 entry = undefined;
87 }
88 }
89 });
90 }
92 > has(model: ITextModel): boolean {
93 return this.all(model).length > 0;
94 }
96 > all(model: ITextModel): T[] {
97 if (!model) {
98 return [];
99 }
100
101 this._updateScores(model, false);
102 const result: T[] = [];
103
104 // from registry
105 for (const entry of this._entries) {
106 if (entry._score > 0) {
107 result.push(entry.provider);
108 }
109 }
110
111 return result;
112 }
114 > allNoModel(): T[] {
115 return this._entries.map(entry => entry.provider);
116 }
118 > get registeredLanguageIds(): ReadonlySet<string> {
119 const result = new Set<string>();
120 for (const entry of this._entries) {
121 selectLanguageIds(entry.selector, result);
122 }
123 return result;
124 }
126 > ordered(model: ITextModel, recursive = false): T[] {
127 const result: T[] = [];
128 this._orderedForEach(model, recursive, entry => result.push(entry.provider));
129 return result;
130 }
132 > orderedGroups(model: ITextModel): T[][] {
133 const result: T[][] = [];
134 let lastBucket: T[];
135 let lastBucketScore: number;
136
137 this._orderedForEach(model, false, entry => {
138 if (lastBucket && lastBucketScore === entry._score) {
139 lastBucket.push(entry.provider);
140 } else {
141 lastBucketScore = entry._score;
142 lastBucket = [entry.provider];
143 result.push(lastBucket);
144 }
145 });
146
147 return result;
148 }
150 > private _orderedForEach(model: ITextModel, recursive: boolean, callback: (provider: Entry<T>) => void): void {
151
152 this._updateScores(model, recursive);
153
154 for (const entry of this._entries) {
155 if (entry._score > 0) {
156 callback(entry);
157 }
158 }
159 }
161 > private _lastCandidate: MatchCandidate | undefined;
162 >
163 > private _updateScores(model: ITextModel, recursive: boolean): void {
164
165 const notebookInfo = this._notebookInfoResolver?.(model.uri);
166
167 // use the uri (scheme, pattern) of the notebook info iff we have one
168 // otherwise it's the model's/document's uri
169 const candidate = notebookInfo
170 ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type, recursive)
171 : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined, recursive);
172
173 if (this._lastCandidate?.equals(candidate)) {
174 // nothing has changed
175 return;
176 }
177
178 this._lastCandidate = candidate;
179
180 for (const entry of this._entries) {
181 entry._score = score(entry.selector, candidate.uri, candidate.languageId, shouldSynchronizeModel(model), candidate.notebookUri, candidate.notebookType);
182
183 if (isExclusive(entry.selector) && entry._score > 0) {
184 if (recursive) {
185 entry._score = 0;
186 } else {
187 // support for one exclusive selector that overwrites
188 // any other selector
189 for (const entry of this._entries) {
190 entry._score = 0;
191 }
192 entry._score = 1000;
193 break;
194 }
195 }
196 }
197
198 // needs sorting
199 this._entries.sort(LanguageFeatureRegistry._compareByScoreAndTime);
200 }
202 > private static _compareByScoreAndTime(a: Entry<unknown>, b: Entry<unknown>): number {
203 if (a._score < b._score) {
204 return 1;
205 } else if (a._score > b._score) {
206 return -1;
207 }
208
209 // De-prioritize built-in providers
210 if (isBuiltinSelector(a.selector) && !isBuiltinSelector(b.selector)) {
211 return 1;
212 } else if (!isBuiltinSelector(a.selector) && isBuiltinSelector(b.selector)) {
213 return -1;
214 }
215
216 if (a._time < b._time) {
217 return 1;
218 } else if (a._time > b._time) {
219 return -1;
220 } else {
221 return 0;
222 }
223 }
225 >
226 function isBuiltinSelector(selector: LanguageSelector): boolean {
227 if (typeof selector === 'string') {
228 return false;
229 }
230
231 if (Array.isArray(selector)) {
232 return selector.some(isBuiltinSelector);
233 }
234
235 return Boolean((selector as LanguageFilter).isBuiltin);
236 }