Skip to content

Commit 4e27a00

Browse files
authored
fix: Load plugin models in vscode extension (#336)
1 parent 3b9e356 commit 4e27a00

File tree

1 file changed

+119
-4
lines changed

1 file changed

+119
-4
lines changed
Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { DefaultWorkspaceManager, LangiumDocument } from 'langium';
1+
import { isPlugin, Model } from '@zenstackhq/language/ast';
2+
import { getLiteral } from '@zenstackhq/sdk';
3+
import { DefaultWorkspaceManager, interruptAndCheck, LangiumDocument } from 'langium';
24
import path from 'path';
3-
import { WorkspaceFolder } from 'vscode-languageserver';
4-
import { URI } from 'vscode-uri';
5-
import { STD_LIB_MODULE_NAME } from './constants';
5+
import { CancellationToken, WorkspaceFolder } from 'vscode-languageserver';
6+
import { URI, Utils } from 'vscode-uri';
7+
import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants';
68

79
/**
810
* Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel
911
*/
1012
export default class ZModelWorkspaceManager extends DefaultWorkspaceManager {
13+
public pluginModels = new Set<string>();
14+
1115
protected async loadAdditionalDocuments(
1216
_folders: WorkspaceFolder[],
1317
_collector: (document: LangiumDocument) => void
@@ -18,4 +22,115 @@ export default class ZModelWorkspaceManager extends DefaultWorkspaceManager {
1822
const stdlib = this.langiumDocuments.getOrCreateDocument(stdLibUri);
1923
_collector(stdlib);
2024
}
25+
26+
override async initializeWorkspace(
27+
folders: WorkspaceFolder[],
28+
cancelToken = CancellationToken.None
29+
): Promise<void> {
30+
const fileExtensions = this.serviceRegistry.all.flatMap((e) => e.LanguageMetaData.fileExtensions);
31+
const documents: LangiumDocument[] = [];
32+
const collector = (document: LangiumDocument) => {
33+
documents.push(document);
34+
if (!this.langiumDocuments.hasDocument(document.uri)) {
35+
this.langiumDocuments.addDocument(document);
36+
}
37+
};
38+
39+
await this.loadAdditionalDocuments(folders, collector);
40+
await Promise.all(
41+
folders
42+
.map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI])
43+
.map(async (entry) => this.traverseFolder(...entry, fileExtensions, collector))
44+
);
45+
46+
// find plugin models
47+
documents.forEach((doc) => {
48+
const parsed = doc.parseResult.value as Model;
49+
parsed.declarations.forEach((decl) => {
50+
if (isPlugin(decl)) {
51+
const providerField = decl.fields.find((f) => f.name === 'provider');
52+
if (providerField) {
53+
const provider = getLiteral<string>(providerField.value);
54+
if (provider) {
55+
this.pluginModels.add(provider);
56+
}
57+
}
58+
}
59+
});
60+
});
61+
62+
if (this.pluginModels.size > 0) {
63+
console.log(`Used plugin documents: ${Array.from(this.pluginModels)}`);
64+
65+
// the loaded plugin models would be removed from the set
66+
const unLoadedPluginModels = new Set(this.pluginModels);
67+
68+
await Promise.all(
69+
folders
70+
.map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI])
71+
.map(async (entry) => this.loadPluginModels(...entry, unLoadedPluginModels, collector))
72+
);
73+
74+
if (unLoadedPluginModels.size > 0) {
75+
console.warn(`The following plugin documents could not be loaded: ${Array.from(unLoadedPluginModels)}`);
76+
}
77+
}
78+
79+
// Only after creating all documents do we check whether we need to cancel the initialization
80+
// The document builder will later pick up on all unprocessed documents
81+
await interruptAndCheck(cancelToken);
82+
await this.documentBuilder.build(documents, undefined, cancelToken);
83+
}
84+
85+
protected async loadPluginModels(
86+
workspaceFolder: WorkspaceFolder,
87+
folderPath: URI,
88+
pluginModels: Set<string>,
89+
collector: (document: LangiumDocument) => void
90+
): Promise<void> {
91+
const content = await (
92+
await this.fileSystemProvider.readDirectory(folderPath)
93+
).sort((a, b) => {
94+
// make sure the node_moudules folder is always the first one to be checked
95+
// so it could be early exited if the plugin is found
96+
if (a.isDirectory && b.isDirectory) {
97+
const aName = Utils.basename(a.uri);
98+
if (aName === 'node_modules') {
99+
return -1;
100+
} else {
101+
return 1;
102+
}
103+
} else {
104+
return 0;
105+
}
106+
});
107+
108+
for (const entry of content) {
109+
if (entry.isDirectory) {
110+
const name = Utils.basename(entry.uri);
111+
if (name === 'node_modules') {
112+
for (const plugin of Array.from(pluginModels)) {
113+
const path = Utils.joinPath(entry.uri, plugin, PLUGIN_MODULE_NAME);
114+
try {
115+
this.fileSystemProvider.readFileSync(path);
116+
const document = this.langiumDocuments.getOrCreateDocument(path);
117+
collector(document);
118+
console.log(`Adding plugin document from ${path.path}`);
119+
120+
pluginModels.delete(plugin);
121+
// early exit if all plugins are loaded
122+
if (pluginModels.size === 0) {
123+
return;
124+
}
125+
} catch {
126+
// no-op. The module might be found in another node_modules folder
127+
// will show the warning message eventually if not found
128+
}
129+
}
130+
} else {
131+
await this.loadPluginModels(workspaceFolder, entry.uri, pluginModels, collector);
132+
}
133+
}
134+
}
135+
}
21136
}

0 commit comments

Comments
 (0)