Skip to content

Commit f53d8c7

Browse files
authored
Watch changes on directories (#1786)
1 parent 6f57f5e commit f53d8c7

21 files changed

+623
-110
lines changed

packages/langium-sprotty/test/trace-provider.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('DefaultTraceProvider', async () => {
3838
node a {
3939
node b {}
4040
}
41-
`, { documentUri: 'test://test.model' });
41+
`, { documentUri: 'test:/test.txt' });
4242
const model = document.parseResult.value;
4343
const source = model.nodes[0].nodes[0];
4444
expect(source).toBeDefined();
@@ -47,20 +47,20 @@ describe('DefaultTraceProvider', async () => {
4747
id: 'node0'
4848
};
4949
services.diagram.TraceProvider.trace(target, source);
50-
expect(target.trace).toBe('test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400');
50+
expect(target.trace).toBe('test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400');
5151
});
5252

5353
test('finds source node', async () => {
5454
const document = await parser(`
5555
node a {
5656
node b {}
5757
}
58-
`, { documentUri: 'test://test.model' });
58+
`, { documentUri: 'test:/test.txt' });
5959
const model = document.parseResult.value;
6060
const target: TracedModelElement = {
6161
type: 'node',
6262
id: 'node0',
63-
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
63+
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
6464
};
6565
const source = services.diagram.TraceProvider.getSource(target);
6666
expect(source).toBeDefined();
@@ -72,7 +72,7 @@ describe('DefaultTraceProvider', async () => {
7272
node a {
7373
node b {}
7474
}
75-
`, { documentUri: 'test://test.model' });
75+
`, { documentUri: 'test:/test.txt' });
7676
const model = document.parseResult.value;
7777
const source = model.nodes[0].nodes[0];
7878
expect(source).toBeDefined();
@@ -87,7 +87,7 @@ describe('DefaultTraceProvider', async () => {
8787
<TracedModelElement>{
8888
type: 'node',
8989
id: 'node1',
90-
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
90+
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
9191
}
9292
]
9393
};
@@ -101,7 +101,7 @@ describe('DefaultTraceProvider', async () => {
101101
node a {
102102
node b {}
103103
}
104-
`, { documentUri: 'test://test.model' });
104+
`, { documentUri: 'test:/test.txt' });
105105
const model = document.parseResult.value;
106106
const source = model.nodes[0].nodes[0];
107107
expect(source).toBeDefined();
@@ -116,7 +116,7 @@ describe('DefaultTraceProvider', async () => {
116116
<TracedModelElement>{
117117
type: 'node',
118118
id: 'node0',
119-
trace: 'test://test.model?1%3A12-3%3A13#%2Fnodes%400'
119+
trace: 'test:/test.txt?1%3A12-3%3A13#%2Fnodes%400'
120120
}
121121
]
122122
};
@@ -130,7 +130,7 @@ describe('DefaultTraceProvider', async () => {
130130
node a {
131131
node b {}
132132
}
133-
`, { documentUri: 'test://test.model' });
133+
`, { documentUri: 'test:/test.txt' });
134134
const model = document.parseResult.value;
135135
const source = model.nodes[0].nodes[0];
136136
expect(source).toBeDefined();
@@ -141,12 +141,12 @@ describe('DefaultTraceProvider', async () => {
141141
<TracedModelElement>{
142142
type: 'node',
143143
id: 'node0',
144-
trace: 'test://test.model?1%3A12-3%3A13#%2Fnodes%400'
144+
trace: 'test:/test.txt?1%3A12-3%3A13#%2Fnodes%400'
145145
},
146146
<TracedModelElement>{
147147
type: 'node',
148148
id: 'node1',
149-
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
149+
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
150150
}
151151
]
152152
};

packages/langium-vscode/src/language-server/grammar-workspace-manager.ts

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

77
import type { Ignore } from 'ignore';
88
import ignore from 'ignore';
9-
import type { ConfigurationProvider, FileSelector, FileSystemNode, WorkspaceFolder } from 'langium';
9+
import type { ConfigurationProvider, FileSystemNode, WorkspaceFolder } from 'langium';
1010
import { Cancellation, DefaultWorkspaceManager, URI, UriUtils } from 'langium';
1111
import type { LangiumSharedServices } from 'langium/lsp';
1212
import * as path from 'path';
@@ -52,15 +52,15 @@ export class LangiumGrammarWorkspaceManager extends DefaultWorkspaceManager {
5252
return super.initializeWorkspace(folders, cancelToken);
5353
}
5454

55-
protected override includeEntry(workspaceFolder: WorkspaceFolder, entry: FileSystemNode, selector: FileSelector): boolean {
56-
if (this.matcher) {
55+
override shouldIncludeEntry(entry: FileSystemNode): boolean {
56+
const workspaceFolder = this.workspaceFolders?.find(folder => UriUtils.contains(folder.uri, entry.uri));
57+
if (this.matcher && workspaceFolder) {
5758
// create path relative to workspace folder root: /user/foo/workspace/entry.txt -> entry.txt
5859
const relPath = path.relative(URI.parse(workspaceFolder.uri).path, entry.uri.path);
5960
const ignored = this.matcher.ignores(relPath);
60-
return !ignored && (entry.isDirectory || (entry.isFile && (selector.fileExtensions.includes(UriUtils.extname(entry.uri)) ||
61-
selector.fileNames.includes(UriUtils.basename(entry.uri)))));
61+
return !ignored && (entry.isDirectory || (entry.isFile && this.serviceRegistry.hasServices(entry.uri)));
6262
}
63-
return super.includeEntry(workspaceFolder, entry, selector);
63+
return super.shouldIncludeEntry(entry);
6464
}
6565

6666
}

packages/langium/src/grammar/internal-grammar-util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export async function createServicesForGrammar<L extends LangiumServices = Langi
184184
};
185185
const languageMetaData = config.languageMetaData ?? {
186186
caseInsensitive: false,
187-
fileExtensions: [`.${grammarNode.name?.toLowerCase() ?? 'unknown'}`],
187+
fileExtensions: ['.txt'],
188188
languageId: grammarNode.name ?? 'UNKNOWN',
189189
mode: 'development'
190190
};

packages/langium/src/lsp/document-update-handler.ts

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -92,43 +92,24 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
9292
}
9393

9494
protected registerFileWatcher(services: LangiumSharedServices): void {
95-
const watchers: FileSystemWatcher[] = [];
96-
// extensions
97-
const fileExtensions = stream(services.ServiceRegistry.all)
98-
.flatMap(language => language.LanguageMetaData.fileExtensions)
99-
.map(ext => ext.startsWith('.') ? ext.substring(1) : ext)
100-
.distinct()
101-
.toArray();
102-
if (fileExtensions.length > 0) {
103-
watchers.push({
104-
globPattern: fileExtensions.length === 1
105-
? `**/*.${fileExtensions[0]}`
106-
: `**/*.{${fileExtensions.join(',')}}`
107-
});
108-
}
109-
// filenames
110-
const fileNames = stream(services.ServiceRegistry.all)
111-
.flatMap(language => language.LanguageMetaData.fileNames ?? [])
112-
.distinct()
113-
.toArray();
114-
if (fileNames.length > 0) {
115-
watchers.push({
116-
globPattern: fileNames.length === 1
117-
? `**/${fileNames[0]}`
118-
: `**/{${fileNames.join(',')}}`
119-
});
120-
}
95+
const watchers = this.getWatchers();
12196
if (watchers.length > 0) {
12297
const connection = services.lsp.Connection;
12398
const options: DidChangeWatchedFilesRegistrationOptions = { watchers };
12499
connection?.client.register(DidChangeWatchedFilesNotification.type, options);
125100
}
126101
}
127102

103+
protected getWatchers(): FileSystemWatcher[] {
104+
return [{
105+
// We need to watch all file changes in the workspace
106+
// Otherwise we miss changes to directories
107+
// This is a limitation of specific language client implementations
108+
globPattern: '**/*'
109+
}];
110+
}
111+
128112
protected fireDocumentUpdate(changed: URI[], deleted: URI[]): void {
129-
// Filter out URIs that do not have a service in the registry
130-
// Running the document builder update will fail for those URIs
131-
changed = changed.filter(uri => this.serviceRegistry.hasServices(uri));
132113
// Only fire the document update when the workspace manager is ready
133114
// Otherwise, we might miss the initial indexing of the workspace
134115
this.workspaceManager.ready.then(() => {

packages/langium/src/node/node-file-system-provider.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ export class NodeFileSystemProvider implements FileSystemProvider {
1515

1616
encoding: NodeTextEncoding = 'utf-8';
1717

18+
async stat(uri: URI): Promise<FileSystemNode> {
19+
const stat = await fs.promises.stat(uri.fsPath);
20+
return {
21+
isFile: stat.isFile(),
22+
isDirectory: stat.isDirectory(),
23+
uri
24+
};
25+
}
26+
27+
statSync(uri: URI): FileSystemNode {
28+
const stat = fs.statSync(uri.fsPath);
29+
return {
30+
isFile: stat.isFile(),
31+
isDirectory: stat.isDirectory(),
32+
uri
33+
};
34+
}
35+
1836
readFile(uri: URI): Promise<string> {
1937
return fs.promises.readFile(uri.fsPath, this.encoding);
2038
}

packages/langium/src/service-registry.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export interface ServiceRegistry {
4141
*/
4242
export class DefaultServiceRegistry implements ServiceRegistry {
4343

44-
protected singleton?: LangiumCoreServices;
4544
protected readonly languageIdMap = new Map<string, LangiumCoreServices>();
4645
protected readonly fileExtensionMap = new Map<string, LangiumCoreServices>();
4746
protected readonly fileNameMap = new Map<string, LangiumCoreServices>();
@@ -76,17 +75,9 @@ export class DefaultServiceRegistry implements ServiceRegistry {
7675
}
7776
}
7877
this.languageIdMap.set(data.languageId, language);
79-
if (this.languageIdMap.size === 1) {
80-
this.singleton = language;
81-
} else {
82-
this.singleton = undefined;
83-
}
8478
}
8579

8680
getServices(uri: URI): LangiumCoreServices {
87-
if (this.singleton !== undefined) {
88-
return this.singleton;
89-
}
9081
if (this.languageIdMap.size === 0) {
9182
throw new Error('The service registry is empty. Use `register` to register the services of a language.');
9283
}

packages/langium/src/test/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
*/
88

99
export * from './langium-test.js';
10+
export * from './virtual-file-system.js';
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/******************************************************************************
2+
* Copyright 2025 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import { URI } from 'vscode-uri';
8+
import type { FileSystemNode, FileSystemProvider } from '../workspace/file-system-provider.js';
9+
import { UriTrie } from '../utils/uri-utils.js';
10+
11+
export class VirtualFileSystemProvider implements FileSystemProvider {
12+
13+
private readonly trie = new UriTrie<string>();
14+
15+
insert(uri: URI | string, content: string): void {
16+
this.trie.insert(uri, content);
17+
}
18+
19+
delete(uri: URI | string): void {
20+
this.trie.delete(uri);
21+
}
22+
23+
stat(uri: URI): Promise<FileSystemNode> {
24+
return Promise.resolve(this.statSync(uri));
25+
}
26+
27+
statSync(uri: URI): FileSystemNode {
28+
const node = this.trie.findNode(uri);
29+
if (node) {
30+
return {
31+
isDirectory: node.element === undefined,
32+
isFile: node.element !== undefined,
33+
uri
34+
};
35+
} else {
36+
throw new Error('File not found');
37+
}
38+
}
39+
40+
readFile(uri: URI): Promise<string> {
41+
const data = this.trie.find(uri);
42+
if (typeof data === 'string') {
43+
return Promise.resolve(data);
44+
} else {
45+
throw new Error('File not found');
46+
}
47+
}
48+
49+
readDirectory(uri: URI): Promise<FileSystemNode[]> {
50+
const node = this.trie.findNode(uri);
51+
if (!node) {
52+
throw new Error('Directory not found');
53+
}
54+
const children = this.trie.findChildren(uri);
55+
return Promise.resolve(children.map(child => ({
56+
isDirectory: child.element === undefined,
57+
isFile: child.element !== undefined,
58+
uri: URI.parse(child.uri)
59+
})));
60+
}
61+
62+
}

0 commit comments

Comments
 (0)