Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch changes on directories #1786

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/langium-sprotty/test/trace-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('DefaultTraceProvider', async () => {
node a {
node b {}
}
`, { documentUri: 'test://test.model' });
`, { documentUri: 'test:/test.txt' });
const model = document.parseResult.value;
const source = model.nodes[0].nodes[0];
expect(source).toBeDefined();
Expand All @@ -47,20 +47,20 @@ describe('DefaultTraceProvider', async () => {
id: 'node0'
};
services.diagram.TraceProvider.trace(target, source);
expect(target.trace).toBe('test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400');
expect(target.trace).toBe('test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400');
});

test('finds source node', async () => {
const document = await parser(`
node a {
node b {}
}
`, { documentUri: 'test://test.model' });
`, { documentUri: 'test:/test.txt' });
const model = document.parseResult.value;
const target: TracedModelElement = {
type: 'node',
id: 'node0',
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
};
const source = services.diagram.TraceProvider.getSource(target);
expect(source).toBeDefined();
Expand All @@ -72,7 +72,7 @@ describe('DefaultTraceProvider', async () => {
node a {
node b {}
}
`, { documentUri: 'test://test.model' });
`, { documentUri: 'test:/test.txt' });
const model = document.parseResult.value;
const source = model.nodes[0].nodes[0];
expect(source).toBeDefined();
Expand All @@ -87,7 +87,7 @@ describe('DefaultTraceProvider', async () => {
<TracedModelElement>{
type: 'node',
id: 'node1',
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
}
]
};
Expand All @@ -101,7 +101,7 @@ describe('DefaultTraceProvider', async () => {
node a {
node b {}
}
`, { documentUri: 'test://test.model' });
`, { documentUri: 'test:/test.txt' });
const model = document.parseResult.value;
const source = model.nodes[0].nodes[0];
expect(source).toBeDefined();
Expand All @@ -116,7 +116,7 @@ describe('DefaultTraceProvider', async () => {
<TracedModelElement>{
type: 'node',
id: 'node0',
trace: 'test://test.model?1%3A12-3%3A13#%2Fnodes%400'
trace: 'test:/test.txt?1%3A12-3%3A13#%2Fnodes%400'
}
]
};
Expand All @@ -130,7 +130,7 @@ describe('DefaultTraceProvider', async () => {
node a {
node b {}
}
`, { documentUri: 'test://test.model' });
`, { documentUri: 'test:/test.txt' });
const model = document.parseResult.value;
const source = model.nodes[0].nodes[0];
expect(source).toBeDefined();
Expand All @@ -141,12 +141,12 @@ describe('DefaultTraceProvider', async () => {
<TracedModelElement>{
type: 'node',
id: 'node0',
trace: 'test://test.model?1%3A12-3%3A13#%2Fnodes%400'
trace: 'test:/test.txt?1%3A12-3%3A13#%2Fnodes%400'
},
<TracedModelElement>{
type: 'node',
id: 'node1',
trace: 'test://test.model?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
trace: 'test:/test.txt?2%3A16-2%3A25#%2Fnodes%400%2Fnodes%400'
}
]
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ export class LangiumGrammarWorkspaceManager extends DefaultWorkspaceManager {
return super.initializeWorkspace(folders, cancelToken);
}

protected override includeEntry(workspaceFolder: WorkspaceFolder, entry: FileSystemNode, fileExtensions: string[]): boolean {
if (this.matcher) {
override includeEntry(entry: FileSystemNode): boolean {
const workspaceFolder = this.workspaceFolders?.find(folder => UriUtils.contains(folder.uri, entry.uri));
if (this.matcher && workspaceFolder) {
// create path relative to workspace folder root: /user/foo/workspace/entry.txt -> entry.txt
const relPath = path.relative(URI.parse(workspaceFolder.uri).path, entry.uri.path);
const ignored = this.matcher.ignores(relPath);
return !ignored && (entry.isDirectory || (entry.isFile && fileExtensions.includes(UriUtils.extname(entry.uri))));
return !ignored && (entry.isDirectory || (entry.isFile && this.serviceRegistry.hasServices(entry.uri)));
}
return super.includeEntry(workspaceFolder, entry, fileExtensions);
return super.includeEntry(entry);
}

}
2 changes: 1 addition & 1 deletion packages/langium/src/grammar/internal-grammar-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export async function createServicesForGrammar<L extends LangiumServices = Langi
};
const languageMetaData = config.languageMetaData ?? {
caseInsensitive: false,
fileExtensions: [`.${grammarNode.name?.toLowerCase() ?? 'unknown'}`],
fileExtensions: ['.txt'],
languageId: grammarNode.name ?? 'UNKNOWN',
mode: 'development'
};
Expand Down
28 changes: 9 additions & 19 deletions packages/langium/src/lsp/document-update-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,28 +92,18 @@ export class DefaultDocumentUpdateHandler implements DocumentUpdateHandler {
}

protected registerFileWatcher(services: LangiumSharedServices): void {
const fileExtensions = stream(services.ServiceRegistry.all)
.flatMap(language => language.LanguageMetaData.fileExtensions)
.map(ext => ext.startsWith('.') ? ext.substring(1) : ext)
.distinct()
.toArray();
if (fileExtensions.length > 0) {
const connection = services.lsp.Connection;
const options: DidChangeWatchedFilesRegistrationOptions = {
watchers: [{
globPattern: fileExtensions.length === 1
? `**/*.${fileExtensions[0]}`
: `**/*.{${fileExtensions.join(',')}}`
}]
};
connection?.client.register(DidChangeWatchedFilesNotification.type, options);
}
const connection = services.lsp.Connection;
const options: DidChangeWatchedFilesRegistrationOptions = {
watchers: [{
// We need to watch all file changes in the workspace
// Otherwise we miss changes to directories
globPattern: '**/*'
}]
};
connection?.client.register(DidChangeWatchedFilesNotification.type, options);
}

protected fireDocumentUpdate(changed: URI[], deleted: URI[]): void {
// Filter out URIs that do not have a service in the registry
// Running the document builder update will fail for those URIs
changed = changed.filter(uri => this.serviceRegistry.hasServices(uri));
// Only fire the document update when the workspace manager is ready
// Otherwise, we might miss the initial indexing of the workspace
this.workspaceManager.ready.then(() => {
Expand Down
18 changes: 18 additions & 0 deletions packages/langium/src/node/node-file-system-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ export class NodeFileSystemProvider implements FileSystemProvider {

encoding: NodeTextEncoding = 'utf-8';

async stat(uri: URI): Promise<FileSystemNode> {
const stat = await fs.promises.stat(uri.fsPath);
return {
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
uri
};
}

statSync(uri: URI): FileSystemNode {
const stat = fs.statSync(uri.fsPath);
return {
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
uri
};
}

readFile(uri: URI): Promise<string> {
return fs.promises.readFile(uri.fsPath, this.encoding);
}
Expand Down
9 changes: 0 additions & 9 deletions packages/langium/src/service-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export interface ServiceRegistry {
*/
export class DefaultServiceRegistry implements ServiceRegistry {

protected singleton?: LangiumCoreServices;
protected readonly languageIdMap = new Map<string, LangiumCoreServices>();
protected readonly fileExtensionMap = new Map<string, LangiumCoreServices>();

Expand All @@ -67,17 +66,9 @@ export class DefaultServiceRegistry implements ServiceRegistry {
this.fileExtensionMap.set(ext, language);
}
this.languageIdMap.set(data.languageId, language);
if (this.languageIdMap.size === 1) {
this.singleton = language;
} else {
this.singleton = undefined;
}
}

getServices(uri: URI): LangiumCoreServices {
if (this.singleton !== undefined) {
return this.singleton;
}
if (this.languageIdMap.size === 0) {
throw new Error('The service registry is empty. Use `register` to register the services of a language.');
}
Expand Down
1 change: 1 addition & 0 deletions packages/langium/src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export * from './langium-test.js';
export * from './virtual-file-system.js';
62 changes: 62 additions & 0 deletions packages/langium/src/test/virtual-file-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/******************************************************************************
* Copyright 2024 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import { URI } from 'vscode-uri';
import type { FileSystemNode, FileSystemProvider } from '../workspace/file-system-provider.js';
import { UriTrie } from '../utils/uri-utils.js';

export class VirtualFileSystemProvider implements FileSystemProvider {

private readonly trie = new UriTrie<string>();

insert(uri: URI | string, content: string): void {
this.trie.insert(uri, content);
}

delete(uri: URI | string): void {
this.trie.delete(uri);
}

stat(uri: URI): Promise<FileSystemNode> {
return Promise.resolve(this.statSync(uri));
}

statSync(uri: URI): FileSystemNode {
const node = this.trie.findNode(uri);
if (node) {
return {
isDirectory: node.element === undefined,
isFile: node.element !== undefined,
uri
};
} else {
throw new Error('File not found');
}
}

readFile(uri: URI): Promise<string> {
const data = this.trie.find(uri);
if (typeof data === 'string') {
return Promise.resolve(data);
} else {
throw new Error('File not found');
}
}

readDirectory(uri: URI): Promise<FileSystemNode[]> {
const node = this.trie.findNode(uri);
if (!node) {
throw new Error('Directory not found');
}
const children = this.trie.findChildren(uri);
return Promise.resolve(children.map(child => ({
isDirectory: child.element === undefined,
isFile: child.element !== undefined,
uri: URI.parse(child.uri)
})));
}

}
Loading
Loading