Skip to content

Commit 5762a08

Browse files
authored
Track unknown documents via language id (#1492)
1 parent 7a02166 commit 5762a08

File tree

3 files changed

+78
-39
lines changed

3 files changed

+78
-39
lines changed

packages/langium/src/default-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export interface DefaultSharedCoreModuleContext {
107107
*/
108108
export function createDefaultSharedCoreModule(context: DefaultSharedCoreModuleContext): Module<LangiumSharedCoreServices, LangiumDefaultSharedCoreServices> {
109109
return {
110-
ServiceRegistry: () => new DefaultServiceRegistry(),
110+
ServiceRegistry: (services) => new DefaultServiceRegistry(services),
111111
workspace: {
112112
LangiumDocuments: (services) => new DefaultLangiumDocuments(services),
113113
LangiumDocumentFactory: (services) => new DefaultLangiumDocumentFactory(services),

packages/langium/src/service-registry.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* terms of the MIT License, which is available in the project root.
55
******************************************************************************/
66

7-
import type { LangiumCoreServices } from './services.js';
7+
import type { LangiumCoreServices, LangiumSharedCoreServices } from './services.js';
8+
import type { TextDocumentProvider } from './workspace/documents.js';
89
import { UriUtils, type URI } from './utils/uri-utils.js';
910

1011
/**
@@ -41,44 +42,60 @@ export interface ServiceRegistry {
4142
export class DefaultServiceRegistry implements ServiceRegistry {
4243

4344
protected singleton?: LangiumCoreServices;
44-
protected map?: Record<string, LangiumCoreServices>;
45+
protected readonly languageIdMap = new Map<string, LangiumCoreServices>();
46+
protected readonly fileExtensionMap = new Map<string, LangiumCoreServices>();
47+
48+
/**
49+
* @deprecated Use the new `fileExtensionMap` (or `languageIdMap`) property instead.
50+
*/
51+
protected get map(): Map<string, LangiumCoreServices> | undefined {
52+
return this.fileExtensionMap;
53+
}
54+
55+
protected readonly textDocuments?: TextDocumentProvider;
56+
57+
constructor(services?: LangiumSharedCoreServices) {
58+
this.textDocuments = services?.workspace.TextDocuments;
59+
}
4560

4661
register(language: LangiumCoreServices): void {
47-
if (!this.singleton && !this.map) {
48-
// This is the first language to be registered; store it as singleton.
49-
this.singleton = language;
50-
return;
51-
}
52-
if (!this.map) {
53-
this.map = {};
54-
if (this.singleton) {
55-
// Move the previous singleton instance to the new map.
56-
for (const ext of this.singleton.LanguageMetaData.fileExtensions) {
57-
this.map[ext] = this.singleton;
58-
}
59-
this.singleton = undefined;
62+
const data = language.LanguageMetaData;
63+
for (const ext of data.fileExtensions) {
64+
if (this.fileExtensionMap.has(ext)) {
65+
console.warn(`The file extension ${ext} is used by multiple languages. It is now assigned to '${data.languageId}'.`);
6066
}
67+
this.fileExtensionMap.set(ext, language);
6168
}
62-
// Store the language services in the map.
63-
for (const ext of language.LanguageMetaData.fileExtensions) {
64-
if (this.map[ext] !== undefined && this.map[ext] !== language) {
65-
console.warn(`The file extension ${ext} is used by multiple languages. It is now assigned to '${language.LanguageMetaData.languageId}'.`);
66-
}
67-
this.map[ext] = language;
69+
this.languageIdMap.set(data.languageId, language);
70+
if (this.languageIdMap.size === 1) {
71+
this.singleton = language;
72+
} else {
73+
this.singleton = undefined;
6874
}
6975
}
7076

7177
getServices(uri: URI): LangiumCoreServices {
7278
if (this.singleton !== undefined) {
7379
return this.singleton;
7480
}
75-
if (this.map === undefined) {
81+
if (this.languageIdMap.size === 0) {
7682
throw new Error('The service registry is empty. Use `register` to register the services of a language.');
7783
}
84+
const languageId = this.textDocuments?.get(uri.toString())?.languageId;
85+
if (languageId !== undefined) {
86+
const services = this.languageIdMap.get(languageId);
87+
if (services) {
88+
return services;
89+
}
90+
}
7891
const ext = UriUtils.extname(uri);
79-
const services = this.map[ext];
92+
const services = this.fileExtensionMap.get(ext);
8093
if (!services) {
81-
throw new Error(`The service registry contains no services for the extension '${ext}'.`);
94+
if (languageId) {
95+
throw new Error(`The service registry contains no services for the extension '${ext}' for language '${languageId}'.`);
96+
} else {
97+
throw new Error(`The service registry contains no services for the extension '${ext}'.`);
98+
}
8299
}
83100
return services;
84101
}
@@ -93,12 +110,6 @@ export class DefaultServiceRegistry implements ServiceRegistry {
93110
}
94111

95112
get all(): readonly LangiumCoreServices[] {
96-
if (this.singleton !== undefined) {
97-
return [this.singleton];
98-
}
99-
if (this.map !== undefined) {
100-
return Object.values(this.map);
101-
}
102-
return [];
113+
return Array.from(this.languageIdMap.values());
103114
}
104115
}

packages/langium/test/service-registry.test.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,57 @@
66

77
/* eslint-disable @typescript-eslint/no-explicit-any */
88

9-
import type { LangiumCoreServices } from 'langium';
9+
import type { LangiumCoreServices, LangiumSharedCoreServices, Module, PartialLangiumSharedCoreServices, TextDocumentProvider } from 'langium';
1010
import { describe, expect, test } from 'vitest';
11-
import { DefaultServiceRegistry, URI } from 'langium';
11+
import { DefaultServiceRegistry, EmptyFileSystem, URI, createDefaultSharedCoreModule, inject, TextDocument } from 'langium';
1212

1313
describe('DefaultServiceRegistry', () => {
1414

1515
test('should work with a single language', () => {
16-
const language: LangiumCoreServices = { foo: 'bar' } as any;
17-
const registry = new DefaultServiceRegistry();
16+
const language: LangiumCoreServices = { LanguageMetaData: { fileExtensions: ['.foo'], languageId: 'foo' } } as any;
17+
const registry = new DefaultServiceRegistry(createSharedCoreServices());
1818
registry.register(language);
1919
expect(registry.getServices(URI.parse('file:/foo.bar'))).toBe(language);
2020
expect(registry.all).toHaveLength(1);
2121
});
2222

2323
test('should work with two languages', () => {
24-
const language1: LangiumCoreServices = { LanguageMetaData: { fileExtensions: ['.foo'] } } as any;
25-
const language2: LangiumCoreServices = { LanguageMetaData: { fileExtensions: ['.bar'] } } as any;
26-
const registry = new DefaultServiceRegistry();
24+
const language1: LangiumCoreServices = { LanguageMetaData: { fileExtensions: ['.foo'], languageId: 'foo' } } as any;
25+
const language2: LangiumCoreServices = { LanguageMetaData: { fileExtensions: ['.bar'], languageId: 'bar' } } as any;
26+
const registry = new DefaultServiceRegistry(createSharedCoreServices());
2727
registry.register(language1);
2828
registry.register(language2);
2929
expect(registry.getServices(URI.parse('file:/test.foo'))).toBe(language1);
3030
expect(registry.getServices(URI.parse('file:/test.bar'))).toBe(language2);
3131
expect(registry.all).toHaveLength(2);
3232
});
3333

34+
test('should work based on language ids', () => {
35+
const language1: LangiumCoreServices = { LanguageMetaData: { fileExtensions: [], languageId: 'foo' } } as any;
36+
const language2: LangiumCoreServices = { LanguageMetaData: { fileExtensions: [], languageId: 'bar' } } as any;
37+
const registry = new DefaultServiceRegistry(createSharedCoreServices('bar'));
38+
registry.register(language1);
39+
registry.register(language2);
40+
expect(registry.getServices(URI.parse('file:/test.x'))).toBe(language2);
41+
expect(registry.getServices(URI.parse('file:/test.y'))).toBe(language2);
42+
expect(registry.all).toHaveLength(2);
43+
});
44+
45+
function createSharedCoreServices(id?: string): LangiumSharedCoreServices {
46+
const textDocumentsModule: Module<LangiumSharedCoreServices, PartialLangiumSharedCoreServices> = {
47+
workspace: {
48+
TextDocuments: (id ? (() => createTextDocuments(id)) : undefined)
49+
}
50+
};
51+
return inject(createDefaultSharedCoreModule(EmptyFileSystem), textDocumentsModule);
52+
}
53+
54+
function createTextDocuments(id: string): TextDocumentProvider {
55+
return {
56+
get(uri: string): TextDocument | undefined {
57+
return TextDocument.create(uri, id, 0, '');
58+
}
59+
};
60+
}
61+
3462
});

0 commit comments

Comments
 (0)