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' ;
2
4
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' ;
6
8
7
9
/**
8
10
* Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel
9
11
*/
10
12
export default class ZModelWorkspaceManager extends DefaultWorkspaceManager {
13
+ public pluginModels = new Set < string > ( ) ;
14
+
11
15
protected async loadAdditionalDocuments (
12
16
_folders : WorkspaceFolder [ ] ,
13
17
_collector : ( document : LangiumDocument ) => void
@@ -18,4 +22,115 @@ export default class ZModelWorkspaceManager extends DefaultWorkspaceManager {
18
22
const stdlib = this . langiumDocuments . getOrCreateDocument ( stdLibUri ) ;
19
23
_collector ( stdlib ) ;
20
24
}
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
+ }
21
136
}
0 commit comments