@likec4/language-server 1.43.0 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/browser/package.json +1 -1
  2. package/browser-worker/package.json +1 -1
  3. package/dist/LikeC4LanguageServices.d.ts +13 -34
  4. package/dist/LikeC4LanguageServices.js +152 -0
  5. package/dist/Rpc.js +257 -0
  6. package/dist/ast.d.ts +5 -1
  7. package/dist/ast.js +257 -0
  8. package/dist/browser-worker.js +4 -0
  9. package/dist/browser.js +35 -0
  10. package/dist/bundled.js +44 -0
  11. package/dist/bundled.mjs +3568 -3900
  12. package/dist/documentation/documentation-provider.js +51 -0
  13. package/dist/documentation/index.js +1 -0
  14. package/dist/empty.js +2 -0
  15. package/dist/filesystem/ChokidarWatcher.d.ts +2 -0
  16. package/dist/filesystem/ChokidarWatcher.js +108 -0
  17. package/dist/filesystem/FileSystemWatcher.js +14 -0
  18. package/dist/filesystem/LikeC4FileSystem.d.ts +1 -2
  19. package/dist/filesystem/LikeC4FileSystem.js +126 -0
  20. package/dist/filesystem/index.d.ts +26 -0
  21. package/dist/filesystem/index.js +29 -0
  22. package/dist/formatting/LikeC4Formatter.js +637 -0
  23. package/dist/formatting/utils.js +18 -0
  24. package/dist/generated/ast.js +2155 -0
  25. package/dist/generated/{grammar.mjs → grammar.js} +6 -2
  26. package/dist/generated/module.d.ts +6 -1
  27. package/dist/generated/module.js +27 -0
  28. package/dist/generated-lib/{icons.mjs → icons.js} +11 -7
  29. package/dist/index.d.ts +10 -1
  30. package/dist/index.js +55 -0
  31. package/dist/{likec4lib.mjs → likec4lib.js} +3 -3
  32. package/dist/logger.js +81 -0
  33. package/dist/lsp/CodeActionProvider.d.ts +14 -0
  34. package/dist/lsp/CodeActionProvider.js +33 -0
  35. package/dist/lsp/CodeLensProvider.js +44 -0
  36. package/dist/lsp/CompletionProvider.d.ts +3 -1
  37. package/dist/lsp/CompletionProvider.js +200 -0
  38. package/dist/lsp/DocumentHighlightProvider.js +10 -0
  39. package/dist/lsp/DocumentLinkProvider.js +58 -0
  40. package/dist/lsp/DocumentSymbolProvider.js +306 -0
  41. package/dist/lsp/HoverProvider.js +106 -0
  42. package/dist/lsp/RenameProvider.js +6 -0
  43. package/dist/lsp/SemanticTokenProvider.js +257 -0
  44. package/dist/lsp/index.d.ts +1 -0
  45. package/dist/lsp/index.js +9 -0
  46. package/dist/mcp/MCPServerFactory.js +73 -0
  47. package/dist/mcp/NoopLikeC4MCPServer.js +17 -0
  48. package/dist/mcp/interfaces.js +5 -0
  49. package/dist/mcp/server/StdioLikeC4MCPServer.js +51 -0
  50. package/dist/mcp/server/StreamableLikeC4MCPServer.js +145 -0
  51. package/dist/mcp/server/WithMCPServer.js +56 -0
  52. package/dist/mcp/tools/_common.d.ts +8 -7
  53. package/dist/mcp/tools/_common.js +49 -0
  54. package/dist/mcp/tools/find-relationships.d.ts +7 -8
  55. package/dist/mcp/tools/find-relationships.js +150 -0
  56. package/dist/mcp/tools/list-projects.d.ts +3 -3
  57. package/dist/mcp/tools/list-projects.js +62 -0
  58. package/dist/mcp/tools/open-view.d.ts +6 -7
  59. package/dist/mcp/tools/open-view.js +52 -0
  60. package/dist/mcp/tools/read-deployment.d.ts +6 -7
  61. package/dist/mcp/tools/read-deployment.js +132 -0
  62. package/dist/mcp/tools/read-element.d.ts +6 -7
  63. package/dist/mcp/tools/read-element.js +194 -0
  64. package/dist/mcp/tools/read-project-summary.d.ts +5 -6
  65. package/dist/mcp/tools/read-project-summary.js +176 -0
  66. package/dist/mcp/tools/read-view.d.ts +6 -7
  67. package/dist/mcp/tools/read-view.js +203 -0
  68. package/dist/mcp/tools/search-element.d.ts +3 -3
  69. package/dist/mcp/tools/search-element.js +177 -0
  70. package/dist/mcp/utils.d.ts +2 -2
  71. package/dist/mcp/utils.js +48 -0
  72. package/dist/model/builder/MergedExtends.js +74 -0
  73. package/dist/model/builder/MergedSpecification.js +175 -0
  74. package/dist/model/builder/buildModel.js +176 -0
  75. package/dist/model/deployments-index.js +102 -0
  76. package/dist/model/fqn-index.d.ts +1 -2
  77. package/dist/model/fqn-index.js +247 -0
  78. package/dist/model/index.js +6 -0
  79. package/dist/model/model-builder.d.ts +13 -11
  80. package/dist/model/model-builder.js +232 -0
  81. package/dist/model/model-locator.d.ts +6 -5
  82. package/dist/model/model-locator.js +240 -0
  83. package/dist/model/model-parser-where.js +81 -0
  84. package/dist/model/model-parser.d.ts +309 -304
  85. package/dist/model/model-parser.js +126 -0
  86. package/dist/model/parser/Base.d.ts +2 -2
  87. package/dist/model/parser/Base.js +367 -0
  88. package/dist/model/parser/DeploymentModelParser.d.ts +2 -2
  89. package/dist/model/parser/DeploymentModelParser.js +176 -0
  90. package/dist/model/parser/DeploymentViewParser.d.ts +3 -3
  91. package/dist/model/parser/DeploymentViewParser.js +86 -0
  92. package/dist/model/parser/FqnRefParser.d.ts +2 -2
  93. package/dist/model/parser/FqnRefParser.js +382 -0
  94. package/dist/model/parser/GlobalsParser.d.ts +6 -6
  95. package/dist/model/parser/GlobalsParser.js +84 -0
  96. package/dist/model/parser/ImportsParser.d.ts +11 -12
  97. package/dist/model/parser/ImportsParser.js +24 -0
  98. package/dist/model/parser/ModelParser.d.ts +2 -2
  99. package/dist/model/parser/ModelParser.js +165 -0
  100. package/dist/model/parser/PredicatesParser.d.ts +2 -2
  101. package/dist/model/parser/PredicatesParser.js +45 -0
  102. package/dist/model/parser/SpecificationParser.d.ts +2 -2
  103. package/dist/model/parser/SpecificationParser.js +113 -0
  104. package/dist/model/parser/ValueConverter.js +12 -0
  105. package/dist/model/parser/ViewsParser.d.ts +3 -3
  106. package/dist/model/parser/ViewsParser.js +479 -0
  107. package/dist/model-change/ModelChanges.d.ts +8 -5
  108. package/dist/model-change/ModelChanges.js +129 -0
  109. package/dist/model-change/changeElementStyle.js +134 -0
  110. package/dist/model-change/changeViewLayout.d.ts +2 -2
  111. package/dist/model-change/changeViewLayout.js +28 -0
  112. package/dist/model-change/removeManualLayoutV1.d.ts +7 -0
  113. package/dist/model-change/removeManualLayoutV1.js +27 -0
  114. package/dist/module.d.ts +10 -5
  115. package/dist/module.js +143 -0
  116. package/dist/protocol.d.ts +34 -27
  117. package/dist/protocol.js +123 -0
  118. package/dist/references/index.js +3 -0
  119. package/dist/references/name-provider.js +37 -0
  120. package/dist/references/scope-computation.js +288 -0
  121. package/dist/references/scope-provider.d.ts +3 -3
  122. package/dist/references/scope-provider.js +242 -0
  123. package/dist/shared/NodeKindProvider.js +57 -0
  124. package/dist/shared/{WorkspaceSymbolProvider.mjs → WorkspaceSymbolProvider.js} +1 -1
  125. package/dist/shared/index.js +2 -0
  126. package/dist/test/index.js +1 -0
  127. package/dist/test/testServices.d.ts +16 -16
  128. package/dist/test/testServices.js +210 -0
  129. package/dist/utils/disposable.js +26 -0
  130. package/dist/utils/elementRef.d.ts +1 -1
  131. package/dist/utils/elementRef.js +27 -0
  132. package/dist/utils/fqnRef.js +63 -0
  133. package/dist/utils/index.js +35 -0
  134. package/dist/utils/printDocs.js +1 -0
  135. package/dist/utils/projectId.js +16 -0
  136. package/dist/utils/stringHash.js +5 -0
  137. package/dist/validation/DocumentValidator.js +17 -0
  138. package/dist/validation/_shared.js +26 -0
  139. package/dist/validation/deployment-checks.js +140 -0
  140. package/dist/validation/dynamic-view.js +67 -0
  141. package/dist/validation/element-ref.js +12 -0
  142. package/dist/validation/element.js +49 -0
  143. package/dist/validation/imports.js +46 -0
  144. package/dist/validation/index.d.ts +1 -1
  145. package/dist/validation/index.js +157 -0
  146. package/dist/validation/property-checks.js +108 -0
  147. package/dist/validation/relation.js +55 -0
  148. package/dist/validation/specification.js +190 -0
  149. package/dist/validation/view-predicates/fqn-expr-with.js +43 -0
  150. package/dist/validation/view-predicates/fqn-ref-expr.js +51 -0
  151. package/dist/validation/view-predicates/incoming.js +16 -0
  152. package/dist/validation/view-predicates/index.js +6 -0
  153. package/dist/validation/view-predicates/outgoing.js +20 -0
  154. package/dist/validation/view-predicates/relation-expr.js +46 -0
  155. package/dist/validation/view-predicates/relation-with.js +16 -0
  156. package/dist/validation/view.d.ts +1 -1
  157. package/dist/validation/view.js +42 -0
  158. package/dist/view-utils/assignNavigateTo.js +27 -0
  159. package/dist/view-utils/index.d.ts +1 -0
  160. package/dist/view-utils/index.js +2 -0
  161. package/dist/view-utils/manual-layout.d.ts +6 -0
  162. package/dist/view-utils/manual-layout.js +149 -0
  163. package/dist/views/ConfigurableLayouter.js +51 -0
  164. package/dist/views/LikeC4ManualLayouts.d.ts +42 -0
  165. package/dist/views/LikeC4ManualLayouts.js +209 -0
  166. package/dist/views/{likec4-views.d.ts → LikeC4Views.d.ts} +32 -10
  167. package/dist/views/LikeC4Views.js +216 -0
  168. package/dist/views/index.d.ts +4 -1
  169. package/dist/views/index.js +11 -0
  170. package/dist/workspace/AstNodeDescriptionProvider.js +18 -0
  171. package/dist/workspace/IndexManager.js +21 -0
  172. package/dist/workspace/LangiumDocuments.d.ts +4 -3
  173. package/dist/workspace/LangiumDocuments.js +72 -0
  174. package/dist/workspace/ProjectsManager.d.ts +25 -16
  175. package/dist/workspace/ProjectsManager.js +469 -0
  176. package/dist/workspace/WorkspaceManager.d.ts +3 -2
  177. package/dist/workspace/WorkspaceManager.js +98 -0
  178. package/dist/workspace/index.js +5 -0
  179. package/likec4lib/package.json +1 -1
  180. package/package.json +30 -28
  181. package/protocol/package.json +1 -1
  182. package/dist/LikeC4LanguageServices.mjs +0 -197
  183. package/dist/Rpc.mjs +0 -296
  184. package/dist/ast.mjs +0 -221
  185. package/dist/browser-worker.mjs +0 -2
  186. package/dist/browser.mjs +0 -32
  187. package/dist/documentation/documentation-provider.mjs +0 -48
  188. package/dist/documentation/index.mjs +0 -1
  189. package/dist/empty.mjs +0 -1
  190. package/dist/filesystem/ChokidarWatcher.mjs +0 -68
  191. package/dist/filesystem/FileSystemWatcher.mjs +0 -11
  192. package/dist/filesystem/LikeC4FileSystem.mjs +0 -64
  193. package/dist/filesystem/index.mjs +0 -19
  194. package/dist/formatting/LikeC4Formatter.mjs +0 -511
  195. package/dist/formatting/utils.mjs +0 -15
  196. package/dist/generated/ast.mjs +0 -2150
  197. package/dist/generated/module.mjs +0 -23
  198. package/dist/index.mjs +0 -50
  199. package/dist/logger.mjs +0 -82
  200. package/dist/lsp/CodeLensProvider.mjs +0 -42
  201. package/dist/lsp/CompletionProvider.mjs +0 -208
  202. package/dist/lsp/DocumentHighlightProvider.mjs +0 -10
  203. package/dist/lsp/DocumentLinkProvider.mjs +0 -53
  204. package/dist/lsp/DocumentSymbolProvider.mjs +0 -287
  205. package/dist/lsp/HoverProvider.mjs +0 -104
  206. package/dist/lsp/RenameProvider.mjs +0 -6
  207. package/dist/lsp/SemanticTokenProvider.mjs +0 -276
  208. package/dist/lsp/index.mjs +0 -7
  209. package/dist/mcp/MCPServerFactory.mjs +0 -70
  210. package/dist/mcp/NoopLikeC4MCPServer.mjs +0 -17
  211. package/dist/mcp/interfaces.mjs +0 -4
  212. package/dist/mcp/server/StdioLikeC4MCPServer.mjs +0 -46
  213. package/dist/mcp/server/StreamableLikeC4MCPServer.mjs +0 -153
  214. package/dist/mcp/server/WithMCPServer.mjs +0 -58
  215. package/dist/mcp/tools/_common.mjs +0 -42
  216. package/dist/mcp/tools/find-relationships.mjs +0 -151
  217. package/dist/mcp/tools/list-projects.mjs +0 -62
  218. package/dist/mcp/tools/open-view.mjs +0 -52
  219. package/dist/mcp/tools/read-deployment.mjs +0 -130
  220. package/dist/mcp/tools/read-element.mjs +0 -198
  221. package/dist/mcp/tools/read-project-summary.mjs +0 -178
  222. package/dist/mcp/tools/read-view.mjs +0 -205
  223. package/dist/mcp/tools/search-element.mjs +0 -171
  224. package/dist/mcp/utils.mjs +0 -47
  225. package/dist/model/builder/MergedExtends.mjs +0 -76
  226. package/dist/model/builder/MergedSpecification.mjs +0 -205
  227. package/dist/model/builder/assignTagColors.d.ts +0 -7
  228. package/dist/model/builder/assignTagColors.mjs +0 -51
  229. package/dist/model/builder/buildModel.mjs +0 -226
  230. package/dist/model/deployments-index.mjs +0 -100
  231. package/dist/model/fqn-index.mjs +0 -243
  232. package/dist/model/index.mjs +0 -6
  233. package/dist/model/model-builder.mjs +0 -285
  234. package/dist/model/model-locator.mjs +0 -239
  235. package/dist/model/model-parser-where.mjs +0 -81
  236. package/dist/model/model-parser.mjs +0 -127
  237. package/dist/model/parser/Base.mjs +0 -376
  238. package/dist/model/parser/DeploymentModelParser.mjs +0 -212
  239. package/dist/model/parser/DeploymentViewParser.mjs +0 -95
  240. package/dist/model/parser/FqnRefParser.mjs +0 -398
  241. package/dist/model/parser/GlobalsParser.mjs +0 -82
  242. package/dist/model/parser/ImportsParser.mjs +0 -28
  243. package/dist/model/parser/ModelParser.mjs +0 -190
  244. package/dist/model/parser/PredicatesParser.mjs +0 -45
  245. package/dist/model/parser/SpecificationParser.mjs +0 -120
  246. package/dist/model/parser/ValueConverter.mjs +0 -12
  247. package/dist/model/parser/ViewsParser.mjs +0 -490
  248. package/dist/model-change/ModelChanges.mjs +0 -89
  249. package/dist/model-change/changeElementStyle.mjs +0 -143
  250. package/dist/model-change/changeViewLayout.mjs +0 -32
  251. package/dist/model-change/saveManualLayout.d.ts +0 -11
  252. package/dist/model-change/saveManualLayout.mjs +0 -27
  253. package/dist/module.mjs +0 -180
  254. package/dist/protocol.mjs +0 -65
  255. package/dist/references/index.mjs +0 -3
  256. package/dist/references/name-provider.mjs +0 -39
  257. package/dist/references/scope-computation.mjs +0 -312
  258. package/dist/references/scope-provider.mjs +0 -239
  259. package/dist/shared/NodeKindProvider.mjs +0 -110
  260. package/dist/shared/index.mjs +0 -2
  261. package/dist/test/index.mjs +0 -1
  262. package/dist/test/testServices.mjs +0 -200
  263. package/dist/utils/disposable.mjs +0 -25
  264. package/dist/utils/elementRef.mjs +0 -20
  265. package/dist/utils/fqnRef.mjs +0 -57
  266. package/dist/utils/index.mjs +0 -33
  267. package/dist/utils/printDocs.mjs +0 -1
  268. package/dist/utils/projectId.mjs +0 -16
  269. package/dist/utils/stringHash.mjs +0 -5
  270. package/dist/validation/DocumentValidator.mjs +0 -16
  271. package/dist/validation/_shared.mjs +0 -25
  272. package/dist/validation/deployment-checks.mjs +0 -146
  273. package/dist/validation/dynamic-view.mjs +0 -67
  274. package/dist/validation/element-ref.mjs +0 -12
  275. package/dist/validation/element.mjs +0 -50
  276. package/dist/validation/imports.mjs +0 -25
  277. package/dist/validation/index.mjs +0 -180
  278. package/dist/validation/property-checks.mjs +0 -107
  279. package/dist/validation/relation.mjs +0 -53
  280. package/dist/validation/specification.mjs +0 -173
  281. package/dist/validation/view-predicates/fqn-expr-with.mjs +0 -43
  282. package/dist/validation/view-predicates/fqn-ref-expr.mjs +0 -53
  283. package/dist/validation/view-predicates/incoming.mjs +0 -16
  284. package/dist/validation/view-predicates/index.mjs +0 -6
  285. package/dist/validation/view-predicates/outgoing.mjs +0 -20
  286. package/dist/validation/view-predicates/relation-expr.mjs +0 -39
  287. package/dist/validation/view-predicates/relation-with.mjs +0 -16
  288. package/dist/validation/view.mjs +0 -25
  289. package/dist/view-utils/assignNavigateTo.mjs +0 -25
  290. package/dist/view-utils/index.mjs +0 -1
  291. package/dist/view-utils/manual-layout.mjs +0 -99
  292. package/dist/views/configurable-layouter.mjs +0 -51
  293. package/dist/views/index.mjs +0 -1
  294. package/dist/views/likec4-views.mjs +0 -166
  295. package/dist/workspace/AstNodeDescriptionProvider.mjs +0 -17
  296. package/dist/workspace/IndexManager.mjs +0 -17
  297. package/dist/workspace/LangiumDocuments.mjs +0 -53
  298. package/dist/workspace/ProjectsManager.mjs +0 -360
  299. package/dist/workspace/WorkspaceManager.mjs +0 -83
  300. package/dist/workspace/index.mjs +0 -5
  301. /package/dist/views/{configurable-layouter.d.ts → ConfigurableLayouter.d.ts} +0 -0
@@ -1,18 +1,19 @@
1
1
  import type { NonEmptyArray, ProjectId } from '@likec4/core';
2
- import type { LangiumDocument, Stream } from 'langium';
2
+ import type { LangiumDocument, Stream, URI } from 'langium';
3
3
  import { DefaultLangiumDocuments } from 'langium';
4
4
  import { type LikeC4LangiumDocument } from '../ast';
5
5
  import type { LikeC4SharedServices } from '../module';
6
6
  export declare class LangiumDocuments extends DefaultLangiumDocuments {
7
7
  protected services: LikeC4SharedServices;
8
- protected compare: any;
8
+ protected compare: (a: string | undefined, b: string | undefined) => number;
9
9
  constructor(services: LikeC4SharedServices);
10
10
  addDocument(document: LangiumDocument): void;
11
+ getDocument(uri: URI): LikeC4LangiumDocument | undefined;
12
+ get all(): Stream<LikeC4LangiumDocument>;
11
13
  /**
12
14
  * Returns all user documents, excluding built-in documents.
13
15
  */
14
16
  get allExcludingBuiltin(): Stream<LikeC4LangiumDocument>;
15
17
  projectDocuments(projectId: ProjectId): Stream<LikeC4LangiumDocument>;
16
18
  groupedByProject(): Record<ProjectId, NonEmptyArray<LikeC4LangiumDocument>>;
17
- resetProjectIds(): void;
18
19
  }
@@ -0,0 +1,72 @@
1
+ import { compareNaturalHierarchically } from '@likec4/core/utils';
2
+ import { DefaultLangiumDocuments, stream } from 'langium';
3
+ import { groupBy, prop } from 'remeda';
4
+ import { isLikeC4LangiumDocument } from '../ast';
5
+ import { LikeC4LanguageMetaData } from '../generated/module';
6
+ import { isLikeC4Builtin } from '../likec4lib';
7
+ /**
8
+ * Compare function for document paths to ensure consistent order
9
+ */
10
+ const compare = compareNaturalHierarchically('/', true);
11
+ const ensureOrder = (a, b) => compare(a.uri.path, b.uri.path);
12
+ const exclude = (doc) => {
13
+ return doc.textDocument.languageId !== LikeC4LanguageMetaData.languageId || isLikeC4Builtin(doc.uri);
14
+ };
15
+ export class LangiumDocuments extends DefaultLangiumDocuments {
16
+ services;
17
+ compare = compareNaturalHierarchically('/', true);
18
+ constructor(services) {
19
+ super(services);
20
+ this.services = services;
21
+ }
22
+ addDocument(document) {
23
+ const uriString = document.uri.toString();
24
+ if (this.documentMap.has(uriString)) {
25
+ throw new Error(`A document with the URI '${uriString}' is already present.`);
26
+ }
27
+ const docs = [...this.documentMap.values(), document].sort(ensureOrder);
28
+ // Clear and re-add documents to ensure consistent order
29
+ this.documentMap.clear();
30
+ for (const doc of docs) {
31
+ this.documentMap.set(doc.uri.toString(), doc);
32
+ }
33
+ }
34
+ getDocument(uri) {
35
+ const doc = super.getDocument(uri);
36
+ if (doc && !exclude(doc)) {
37
+ doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
38
+ }
39
+ if (doc && !isLikeC4LangiumDocument(doc)) {
40
+ throw new Error(`Document ${doc.uri.path} is not a LikeC4 document`);
41
+ }
42
+ return doc;
43
+ }
44
+ get all() {
45
+ return stream(this.documentMap.values())
46
+ .filter((doc) => {
47
+ if (doc.textDocument.languageId === LikeC4LanguageMetaData.languageId) {
48
+ if (!isLikeC4Builtin(doc.uri)) {
49
+ doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
50
+ }
51
+ return true;
52
+ }
53
+ return false;
54
+ });
55
+ }
56
+ /**
57
+ * Returns all user documents, excluding built-in documents.
58
+ */
59
+ get allExcludingBuiltin() {
60
+ const projects = this.services.workspace.ProjectsManager;
61
+ return super.all.filter((doc) => {
62
+ // Exclude built-in and non-LikeC4 documents, and also documents excluded by ProjectsManager
63
+ return !exclude(doc) && !projects.isExcluded(doc);
64
+ });
65
+ }
66
+ projectDocuments(projectId) {
67
+ return this.allExcludingBuiltin.filter(doc => doc.likec4ProjectId === projectId);
68
+ }
69
+ groupedByProject() {
70
+ return groupBy(this.allExcludingBuiltin.toArray(), prop('likec4ProjectId'));
71
+ }
72
+ }
@@ -1,6 +1,6 @@
1
1
  import { type LikeC4ProjectConfig, type LikeC4ProjectConfigInput } from '@likec4/config';
2
2
  import type { NonEmptyReadonlyArray } from '@likec4/core';
3
- import type { scalar } from '@likec4/core/types';
3
+ import type { ProjectId, scalar } from '@likec4/core/types';
4
4
  import { type Cancellation, type LangiumDocument, URI, WorkspaceCache } from 'langium';
5
5
  import type { Tagged } from 'type-fest';
6
6
  import type { LikeC4SharedServices } from '../module';
@@ -29,7 +29,7 @@ export declare class ProjectsManager {
29
29
  * The global project ID used for all documents
30
30
  * that are not part of a specific project.
31
31
  */
32
- static readonly DefaultProjectId: scalar.ProjectId;
32
+ static readonly DefaultProjectId: ProjectId<string>;
33
33
  constructor(services: LikeC4SharedServices);
34
34
  /**
35
35
  * Returns:
@@ -40,6 +40,7 @@ export declare class ProjectsManager {
40
40
  */
41
41
  get defaultProjectId(): scalar.ProjectId | undefined;
42
42
  set defaultProjectId(id: string | scalar.ProjectId | undefined);
43
+ get default(): ProjectData;
43
44
  get all(): NonEmptyReadonlyArray<scalar.ProjectId>;
44
45
  getProject(arg: scalar.ProjectId | LangiumDocument): Project;
45
46
  /**
@@ -48,11 +49,15 @@ export declare class ProjectsManager {
48
49
  * If there are multiple projects and default project is not set, throws an error
49
50
  */
50
51
  ensureProjectId(projectId?: scalar.ProjectId | undefined): scalar.ProjectId;
52
+ /**
53
+ * Validates and ensures the project.
54
+ */
55
+ ensureProject(projectId?: scalar.ProjectId | undefined): Project;
51
56
  hasMultipleProjects(): boolean;
52
57
  /**
53
58
  * Checks if the specified document should be excluded from processing.
54
59
  */
55
- checkIfExcluded(document: LangiumDocument | URI | string): boolean;
60
+ isExcluded(document: LangiumDocument | URI | string): boolean;
56
61
  /**
57
62
  * Checks if it is a config file and it is not excluded by default exclude pattern
58
63
  *
@@ -60,30 +65,34 @@ export declare class ProjectsManager {
60
65
  */
61
66
  isConfigFile(entry: URI): boolean;
62
67
  /**
63
- * Checks if the provided file system entry is a valid project config file.
64
- *
65
- * @param entry The file system entry to check
68
+ * Registers likec4 project by config file.
66
69
  */
67
- registerConfigFile(configFile: URI): Promise<ProjectData | undefined>;
70
+ registerConfigFile(configFile: URI): Promise<ProjectData>;
68
71
  /**
69
72
  * Registers (or reloads) likec4 project by config file or config object.
70
73
  * If there is some project registered at same folder, it will be reloaded.
71
74
  */
72
- registerProject(opts: URI | {
73
- config: LikeC4ProjectConfigInput;
75
+ registerProject(opts: {
76
+ config: LikeC4ProjectConfig | LikeC4ProjectConfigInput;
74
77
  folderUri: URI | string;
75
78
  }): Promise<ProjectData>;
76
79
  /**
77
- * Registers (or reloads) likec4 project by config file or config object.
78
- * If there is some project registered at same folder, it will be reloaded.
80
+ * Determines which project the given document belongs to.
81
+ * If the document does not belong to any project, returns the default project ID.
79
82
  */
80
- private _registerProject;
81
83
  belongsTo(document: LangiumDocument | URI | string): scalar.ProjectId;
82
84
  reloadProjects(): Promise<void>;
85
+ protected _reloadProjects(): Promise<void>;
83
86
  protected uniqueProjectId(name: string): scalar.ProjectId;
84
- protected resetProjectIds(): void;
85
- protected rebuidDocuments(cancelToken?: Cancellation.CancellationToken): Promise<void>;
86
- protected findProjectForDocument(documentUri: string): any;
87
- protected get mappingsToProject(): WorkspaceCache<string, Pick<ProjectData, 'id' | 'config' | 'exclude'>>;
87
+ protected reset(): void;
88
+ rebuidProject(projectId: ProjectId, cancelToken?: Cancellation.CancellationToken): Promise<void>;
89
+ protected findProjectForDocument(documentUri: string): ProjectData;
90
+ protected get mappingsToProject(): WorkspaceCache<string, ProjectData>;
91
+ /**
92
+ * The mapping between documents and projects they belong to.
93
+ * Lazy-created due to initialization order of the LanguageServer
94
+ */
95
+ protected get documentBelongsTo(): WorkspaceCache<LangiumDocument, ProjectData>;
96
+ private getWorkspaceFolder;
88
97
  }
89
98
  export {};
@@ -0,0 +1,469 @@
1
+ import { isLikeC4Config, validateProjectConfig, } from '@likec4/config';
2
+ import { BiMap, delay, invariant, memoizeProp, nonNullable } from '@likec4/core/utils';
3
+ import { wrapError } from '@likec4/log';
4
+ import { deepEqual } from 'fast-equals';
5
+ import { interruptAndCheck, URI, WorkspaceCache, } from 'langium';
6
+ import picomatch from 'picomatch';
7
+ import { hasAtLeast, isNullish, map, pipe, prop, sortBy } from 'remeda';
8
+ import { joinRelativeURL, parseFilename, withoutProtocol, withTrailingSlash, } from 'ufo';
9
+ import { isLikeC4Builtin } from '../likec4lib';
10
+ import { logger as mainLogger } from '../logger';
11
+ const logger = mainLogger.getChild('ProjectsManager');
12
+ function normalizeUri(uri) {
13
+ if (URI.isUri(uri)) {
14
+ return uri.toString();
15
+ }
16
+ else if (typeof uri === 'string') {
17
+ // TODO: handle non-file URIs, i.e. vscode-remote://
18
+ return uri.startsWith('file://') ? uri : URI.file(uri).toString();
19
+ }
20
+ else {
21
+ return uri.uri.toString();
22
+ }
23
+ }
24
+ export function ProjectFolder(folder) {
25
+ folder = normalizeUri(folder);
26
+ return withTrailingSlash(folder);
27
+ }
28
+ const DefaultProject = {
29
+ id: 'default',
30
+ config: {
31
+ name: 'default',
32
+ exclude: ['**/node_modules/**'],
33
+ },
34
+ exclude: picomatch('**/node_modules/**', { dot: true }),
35
+ };
36
+ export class ProjectsManager {
37
+ services;
38
+ /**
39
+ * The global project ID used for all documents
40
+ * that are not part of a specific project.
41
+ */
42
+ static DefaultProjectId = DefaultProject.id;
43
+ /**
44
+ * Configured default project ID.
45
+ * (it is used in CLI and Vite plugin)
46
+ */
47
+ #defaultProjectId = undefined;
48
+ /**
49
+ * Cached default project.
50
+ */
51
+ #defaultProject = undefined;
52
+ /**
53
+ * The mapping between project config files and project IDs.
54
+ */
55
+ #projectIdToFolder = new BiMap();
56
+ /**
57
+ * Registered projects.
58
+ * Sorted descending by the number of segments in the folder path.
59
+ * This ensures that the most specific project is used for a document.
60
+ */
61
+ #projects = [];
62
+ #excludedDocuments = new WeakMap();
63
+ constructor(services) {
64
+ this.services = services;
65
+ logger.debug `created`;
66
+ }
67
+ /**
68
+ * Returns:
69
+ * - configured default project ID if set
70
+ * - the default project ID if there are no projects.
71
+ * - the ID of the only project
72
+ * - undefined if there are multiple projects.
73
+ */
74
+ get defaultProjectId() {
75
+ if (this.#defaultProjectId) {
76
+ return this.#defaultProjectId;
77
+ }
78
+ if (this.#projects.length > 1) {
79
+ return undefined;
80
+ }
81
+ return this.#projects[0]?.id ?? ProjectsManager.DefaultProjectId;
82
+ }
83
+ set defaultProjectId(id) {
84
+ if (id === this.#defaultProjectId) {
85
+ return;
86
+ }
87
+ this.#defaultProject = undefined;
88
+ if (!id || id === ProjectsManager.DefaultProjectId) {
89
+ logger.debug `reset default project ID`;
90
+ this.#defaultProjectId = undefined;
91
+ return;
92
+ }
93
+ invariant(this.#projects.find(p => p.id === id), `Project "${id}" not found`);
94
+ logger.debug `set default project ID to ${id}`;
95
+ this.#defaultProjectId = id;
96
+ }
97
+ get default() {
98
+ if (!this.#defaultProject) {
99
+ const id = this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
100
+ let project = this.#projects.find(p => p.id === id);
101
+ if (!project) {
102
+ const folderUri = this.getWorkspaceFolder();
103
+ project = {
104
+ id,
105
+ config: DefaultProject.config,
106
+ folder: ProjectFolder(folderUri),
107
+ folderUri,
108
+ exclude: DefaultProject.exclude,
109
+ };
110
+ }
111
+ this.#defaultProject = project;
112
+ }
113
+ return this.#defaultProject;
114
+ }
115
+ get all() {
116
+ if (hasAtLeast(this.#projects, 1)) {
117
+ const ids = [
118
+ ...map(this.#projects, prop('id')),
119
+ DefaultProject.id,
120
+ ];
121
+ // if default project is set, ensure it is first
122
+ if (this.#defaultProjectId) {
123
+ const idx = ids.findIndex(p => p === this.#defaultProjectId);
124
+ if (idx > 0) {
125
+ const [defaultProject] = ids.splice(idx, 1);
126
+ return [defaultProject, ...ids];
127
+ }
128
+ }
129
+ return ids;
130
+ }
131
+ return [DefaultProject.id];
132
+ }
133
+ getProject(arg) {
134
+ const id = typeof arg === 'string' ? arg : (arg.likec4ProjectId || this.belongsTo(arg));
135
+ if (id === DefaultProject.id) {
136
+ let folderUri;
137
+ try {
138
+ folderUri = this.services.workspace.WorkspaceManager.workspaceUri;
139
+ }
140
+ catch (error) {
141
+ logger.warn('Failed to get workspace URI, using default folder', { error });
142
+ folderUri = URI.file('/');
143
+ // ignore - workspace not initialized
144
+ }
145
+ return {
146
+ id: ProjectsManager.DefaultProjectId,
147
+ config: DefaultProject.config,
148
+ folderUri,
149
+ };
150
+ }
151
+ const { config, folderUri, } = nonNullable(this.#projects.find(p => p.id === id), `Project "${id}" not found`);
152
+ return {
153
+ id,
154
+ folderUri,
155
+ config,
156
+ };
157
+ }
158
+ /**
159
+ * Validates and ensures the project ID.
160
+ * If no project ID is specified, returns default project ID
161
+ * If there are multiple projects and default project is not set, throws an error
162
+ */
163
+ ensureProjectId(projectId) {
164
+ if (projectId === ProjectsManager.DefaultProjectId) {
165
+ return this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
166
+ }
167
+ if (projectId) {
168
+ invariant(this.#projectIdToFolder.has(projectId), `Project ID ${projectId} is not registered`);
169
+ return projectId;
170
+ }
171
+ return nonNullable(this.defaultProjectId, () => `Specify exact project, known: [${[...this.#projectIdToFolder.keys()].join(', ')}]`);
172
+ }
173
+ /**
174
+ * Validates and ensures the project.
175
+ */
176
+ ensureProject(projectId) {
177
+ projectId = this.ensureProjectId(projectId);
178
+ return this.getProject(projectId);
179
+ }
180
+ hasMultipleProjects() {
181
+ return this.#projects.length > 1;
182
+ }
183
+ /**
184
+ * Checks if the specified document should be excluded from processing.
185
+ */
186
+ isExcluded(document) {
187
+ if (typeof document === 'string' || URI.isUri(document)) {
188
+ let docUriAsString = normalizeUri(document);
189
+ const project = this.findProjectForDocument(docUriAsString);
190
+ return project.exclude ? project.exclude(withoutProtocol(docUriAsString)) : false;
191
+ }
192
+ let isExcluded = this.#excludedDocuments.get(document);
193
+ if (isExcluded === undefined) {
194
+ isExcluded = this.isExcluded(document.uri);
195
+ this.#excludedDocuments.set(document, isExcluded);
196
+ }
197
+ return isExcluded;
198
+ }
199
+ /**
200
+ * Checks if it is a config file and it is not excluded by default exclude pattern
201
+ *
202
+ * @param entry The file system entry to check
203
+ */
204
+ isConfigFile(entry) {
205
+ const filename = parseFilename(entry.toString(), { strict: false })?.toLowerCase();
206
+ const isConfigFile = !!filename && isLikeC4Config(filename);
207
+ if (isConfigFile) {
208
+ if (DefaultProject.exclude(entry.path)) {
209
+ logger.debug `exclude config file ${entry.path}`;
210
+ return false;
211
+ }
212
+ }
213
+ return isConfigFile;
214
+ }
215
+ /**
216
+ * Registers likec4 project by config file.
217
+ */
218
+ async registerConfigFile(configFile) {
219
+ if (DefaultProject.exclude(configFile.path)) {
220
+ throw new Error(`Path to ${configFile.fsPath} is excluded by: ${DefaultProject.config.exclude.join(', ')}`);
221
+ }
222
+ if (!this.isConfigFile(configFile)) {
223
+ throw new Error(`${configFile.fsPath} is not a valid LikeC4 config filename.`);
224
+ }
225
+ try {
226
+ const config = await this.services.workspace.FileSystemProvider.loadProjectConfig(configFile);
227
+ const path = joinRelativeURL(configFile.path, '..');
228
+ const folderUri = configFile.with({ path });
229
+ return await this.registerProject({ config, folderUri });
230
+ }
231
+ catch (error) {
232
+ this.services.lsp.Connection?.window.showErrorMessage(`LikeC4: Failed to register project at ${configFile.fsPath}`);
233
+ throw wrapError(error, `Failed to register project config ${configFile.fsPath}:\n`);
234
+ }
235
+ }
236
+ /**
237
+ * Registers (or reloads) likec4 project by config file or config object.
238
+ * If there is some project registered at same folder, it will be reloaded.
239
+ */
240
+ async registerProject(opts) {
241
+ const config = validateProjectConfig(opts.config);
242
+ const folder = ProjectFolder(opts.folderUri);
243
+ let project = this.#projects.find(p => p.folder === folder);
244
+ if (project && deepEqual(project.config, config)) {
245
+ return project;
246
+ }
247
+ let mustReset = false;
248
+ let id;
249
+ if (!project) {
250
+ if (this.#projectIdToFolder.has(config.name)) {
251
+ logger.warn `Project "${config.name}" already exists, generating unique ID`;
252
+ }
253
+ id = this.uniqueProjectId(config.name);
254
+ project = {
255
+ id,
256
+ config,
257
+ folder,
258
+ folderUri: URI.parse(folder),
259
+ };
260
+ // if there is any project within subfolder or parent folder
261
+ // we need to reset assigned to documents project IDs
262
+ mustReset = this.#projects.some(p => p.folder.startsWith(folder) || folder.startsWith(p.folder));
263
+ this.#projects = pipe([...this.#projects, project], sortBy([({ folder }) => withoutProtocol(folder).split('/').length, 'desc']));
264
+ logger.info `register project ${project.id} folder: ${folder}`;
265
+ }
266
+ else {
267
+ // Project exists but configs are different (deepEqual check above)
268
+ mustReset = true;
269
+ if (project.config.name !== config.name) {
270
+ this.#projectIdToFolder.delete(project.id);
271
+ logger.info `unregister project ${project.id} folder: ${folder}`;
272
+ id = this.uniqueProjectId(config.name);
273
+ project.id = id;
274
+ logger.info `re-register project ${project.id} folder: ${folder}`;
275
+ }
276
+ else {
277
+ id = project.id;
278
+ logger.info `update project ${project.id} on config change`;
279
+ }
280
+ project.config = config;
281
+ }
282
+ // Reset cached default project
283
+ this.#defaultProject = undefined;
284
+ if (isNullish(config.exclude)) {
285
+ project.exclude = DefaultProject.exclude;
286
+ }
287
+ else if (hasAtLeast(config.exclude, 1)) {
288
+ project.exclude = picomatch(config.exclude, { dot: true });
289
+ }
290
+ this.#projectIdToFolder.set(project.id, folder);
291
+ // Reset assigned project IDs if no projects reload is active
292
+ if (mustReset && !this.#activeReload) {
293
+ await this.rebuidProject(project.id).catch(error => {
294
+ logger.warn('Failed to rebuild project {projectId} after config change', {
295
+ projectId: project.id,
296
+ error,
297
+ });
298
+ });
299
+ }
300
+ return project;
301
+ }
302
+ /**
303
+ * Determines which project the given document belongs to.
304
+ * If the document does not belong to any project, returns the default project ID.
305
+ */
306
+ belongsTo(document) {
307
+ if (URI.isUri(document) || typeof document === 'string') {
308
+ const documentUri = normalizeUri(document);
309
+ return this.findProjectForDocument(documentUri).id;
310
+ }
311
+ return this.documentBelongsTo.get(document, () => {
312
+ return this.findProjectForDocument(normalizeUri(document.uri));
313
+ }).id;
314
+ }
315
+ #activeReload = null;
316
+ async reloadProjects() {
317
+ try {
318
+ if (!this.#activeReload) {
319
+ logger.debug `schedule reload projects`;
320
+ this.#activeReload = this._reloadProjects();
321
+ }
322
+ else {
323
+ logger.debug `reload projects is already in progress, waiting`;
324
+ }
325
+ await this.#activeReload;
326
+ }
327
+ finally {
328
+ this.#activeReload = null;
329
+ }
330
+ }
331
+ async _reloadProjects() {
332
+ // debounce reload projects
333
+ await delay(200);
334
+ const folders = this.services.workspace.WorkspaceManager.workspaceFolders;
335
+ if (!folders) {
336
+ logger.warn('No workspace folders found');
337
+ return;
338
+ }
339
+ await this.services.workspace.WorkspaceLock.write(async (cancelToken) => {
340
+ logger.debug `start reload projects`;
341
+ const configFiles = [];
342
+ for (const folder of folders) {
343
+ try {
344
+ logger.debug `scan projects in folder ${folder.uri}`;
345
+ const files = await this.services.workspace.FileSystemProvider.scanProjectFiles(URI.parse(folder.uri));
346
+ for (const file of files) {
347
+ if (file.isFile && this.isConfigFile(file.uri)) {
348
+ logger.debug `found config ${file.uri.fsPath}`;
349
+ configFiles.push(file.uri);
350
+ }
351
+ }
352
+ }
353
+ catch (error) {
354
+ logger.error('Failed to scanProjectFiles, {folder}', { folder: folder.uri, error });
355
+ }
356
+ }
357
+ if (configFiles.length === 0 && this.#projects.length !== 0) {
358
+ logger.warning('No config files found, but some projects were registered before');
359
+ }
360
+ await interruptAndCheck(cancelToken);
361
+ this.#projects = [];
362
+ this.#projectIdToFolder.clear();
363
+ for (const uri of configFiles) {
364
+ try {
365
+ await this.registerConfigFile(uri);
366
+ }
367
+ catch (error) {
368
+ logger.error('Failed to load config file {uri}', { uri: uri.fsPath, error });
369
+ }
370
+ }
371
+ this.reset();
372
+ await this.services.workspace.WorkspaceManager.rebuildAll(cancelToken);
373
+ });
374
+ }
375
+ uniqueProjectId(name) {
376
+ let id = name;
377
+ let i = 1;
378
+ while (this.#projectIdToFolder.has(id)) {
379
+ id = `${name}-${i++}`;
380
+ }
381
+ return id;
382
+ }
383
+ reset() {
384
+ this.#defaultProject = undefined;
385
+ if (this.#defaultProjectId && !this.#projectIdToFolder.has(this.#defaultProjectId)) {
386
+ this.#defaultProjectId = undefined;
387
+ }
388
+ this.services.workspace.LangiumDocuments.all.forEach(doc => {
389
+ if (isLikeC4Builtin(doc.uri)) {
390
+ return;
391
+ }
392
+ // Remove assigned project ID to force re-calculation
393
+ delete doc.likec4ProjectId;
394
+ });
395
+ this.documentBelongsTo.clear();
396
+ this.mappingsToProject.clear();
397
+ this.#excludedDocuments = new WeakMap();
398
+ }
399
+ async rebuidProject(projectId, cancelToken) {
400
+ if (!cancelToken) {
401
+ return await this.services.workspace.WorkspaceLock.write(async (ct) => {
402
+ await this.rebuidProject(projectId, ct);
403
+ });
404
+ }
405
+ // reset default project cache
406
+ this.#defaultProject = undefined;
407
+ const project = this.#projects.find(p => p.id === projectId) ?? this.default;
408
+ const folder = project.folder;
409
+ const docs = this.services.workspace.LangiumDocuments
410
+ .all
411
+ .filter(doc => {
412
+ if (project.exclude?.(doc.uri.path)) {
413
+ return false;
414
+ }
415
+ if (doc.uri.toString().startsWith(folder)) {
416
+ return true;
417
+ }
418
+ const docdir = withTrailingSlash(joinRelativeURL(doc.uri.toString(), '..'));
419
+ return docdir.startsWith(folder) || folder.startsWith(docdir);
420
+ })
421
+ .map(d => d.uri)
422
+ .toArray();
423
+ if (docs.length > 0) {
424
+ this.reset();
425
+ const projectId = project.id;
426
+ logger.info('rebuild documents of project {projectId}: {docs}', {
427
+ projectId,
428
+ docs: docs.length,
429
+ });
430
+ await this.services.workspace.DocumentBuilder
431
+ .update(docs, [], cancelToken)
432
+ .catch(error => {
433
+ logger.warn('Failed to rebuild project {projectId}', {
434
+ projectId,
435
+ error,
436
+ });
437
+ });
438
+ }
439
+ }
440
+ findProjectForDocument(documentUri) {
441
+ return this.mappingsToProject.get(documentUri, () => {
442
+ const project = this.#projects.find(({ folder }) => documentUri.startsWith(folder));
443
+ // If the document is not part of any project, assign it to the global project ID
444
+ return project ?? this.default;
445
+ });
446
+ }
447
+ // The mapping between document URIs and their corresponding project ID
448
+ // Lazy-created due to initialization order of the LanguageServer
449
+ get mappingsToProject() {
450
+ return memoizeProp(this, '_mappingsToProject', () => new WorkspaceCache(this.services));
451
+ }
452
+ /**
453
+ * The mapping between documents and projects they belong to.
454
+ * Lazy-created due to initialization order of the LanguageServer
455
+ */
456
+ get documentBelongsTo() {
457
+ return memoizeProp(this, '_documentBelongsTo', () => new WorkspaceCache(this.services));
458
+ }
459
+ getWorkspaceFolder() {
460
+ try {
461
+ return this.services.workspace.WorkspaceManager.workspaceUri;
462
+ }
463
+ catch (error) {
464
+ logger.warn('Failed to get workspace URI, using default folder', { error });
465
+ return URI.file('/');
466
+ // ignore - workspace not initialized
467
+ }
468
+ }
469
+ }
@@ -1,4 +1,4 @@
1
- import type { BuildOptions, FileSelector, FileSystemNode, LangiumDocument, LangiumDocumentFactory } from 'langium';
1
+ import type { BuildOptions, Cancellation, FileSelector, FileSystemNode, LangiumDocument, LangiumDocumentFactory } from 'langium';
2
2
  import { DefaultWorkspaceManager } from 'langium';
3
3
  import type { WorkspaceFolder } from 'vscode-languageserver';
4
4
  import { URI } from 'vscode-uri';
@@ -24,7 +24,8 @@ export declare class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
24
24
  * Determine whether the given folder entry shall be included while indexing the workspace.
25
25
  */
26
26
  protected includeEntry(_workspaceFolder: WorkspaceFolder, entry: FileSystemNode, selector: FileSelector): boolean;
27
- workspace(): any;
27
+ workspace(): WorkspaceFolder | null;
28
+ rebuildAll(cancelToken?: Cancellation.CancellationToken): Promise<void>;
28
29
  get workspaceUri(): URI;
29
30
  get workspaceURL(): URL;
30
31
  }