@likec4/language-server 1.27.3 → 1.28.1

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 (113) hide show
  1. package/dist/LikeC4LanguageServices.js +6 -7
  2. package/dist/ast.d.ts +16 -9
  3. package/dist/ast.js +58 -79
  4. package/dist/bundled.mjs +2161 -2141
  5. package/dist/config/schema.d.ts +3 -3
  6. package/dist/config/schema.js +12 -5
  7. package/dist/documentation/documentation-provider.js +3 -1
  8. package/dist/formatting/LikeC4Formatter.d.ts +0 -2
  9. package/dist/formatting/LikeC4Formatter.js +24 -53
  10. package/dist/generated/ast.d.ts +128 -233
  11. package/dist/generated/ast.js +136 -308
  12. package/dist/generated/grammar.js +1 -1
  13. package/dist/lsp/CompletionProvider.d.ts +3 -0
  14. package/dist/lsp/CompletionProvider.js +128 -113
  15. package/dist/lsp/DocumentLinkProvider.js +6 -3
  16. package/dist/lsp/HoverProvider.js +3 -1
  17. package/dist/lsp/SemanticTokenProvider.js +33 -43
  18. package/dist/model/builder/MergedSpecification.d.ts +5 -3
  19. package/dist/model/builder/MergedSpecification.js +21 -7
  20. package/dist/model/builder/buildModel.d.ts +6 -1
  21. package/dist/model/builder/buildModel.js +20 -15
  22. package/dist/model/deployments-index.js +4 -2
  23. package/dist/model/fqn-index.d.ts +4 -2
  24. package/dist/model/fqn-index.js +28 -5
  25. package/dist/model/model-builder.d.ts +2 -2
  26. package/dist/model/model-builder.js +54 -16
  27. package/dist/model/model-locator.js +7 -4
  28. package/dist/model/model-parser.d.ts +215 -52
  29. package/dist/model/model-parser.js +6 -2
  30. package/dist/model/parser/Base.d.ts +11 -2
  31. package/dist/model/parser/Base.js +138 -3
  32. package/dist/model/parser/DeploymentModelParser.d.ts +19 -2
  33. package/dist/model/parser/DeploymentModelParser.js +19 -29
  34. package/dist/model/parser/DeploymentViewParser.d.ts +18 -2
  35. package/dist/model/parser/DeploymentViewParser.js +6 -24
  36. package/dist/model/parser/FqnRefParser.d.ts +18 -3
  37. package/dist/model/parser/FqnRefParser.js +264 -40
  38. package/dist/model/parser/GlobalsParser.d.ts +35 -18
  39. package/dist/model/parser/ImportsParser.d.ts +32 -0
  40. package/dist/model/parser/ImportsParser.js +26 -0
  41. package/dist/model/parser/ModelParser.d.ts +26 -2
  42. package/dist/model/parser/ModelParser.js +21 -41
  43. package/dist/model/parser/PredicatesParser.d.ts +35 -12
  44. package/dist/model/parser/PredicatesParser.js +20 -271
  45. package/dist/model/parser/SpecificationParser.d.ts +8 -0
  46. package/dist/model/parser/SpecificationParser.js +5 -9
  47. package/dist/model/parser/ViewsParser.d.ts +36 -19
  48. package/dist/model/parser/ViewsParser.js +16 -12
  49. package/dist/model-change/changeElementStyle.d.ts +2 -2
  50. package/dist/model-change/changeElementStyle.js +9 -6
  51. package/dist/references/name-provider.js +8 -2
  52. package/dist/references/scope-computation.d.ts +1 -1
  53. package/dist/references/scope-computation.js +33 -3
  54. package/dist/references/scope-provider.d.ts +7 -8
  55. package/dist/references/scope-provider.js +59 -41
  56. package/dist/shared/NodeKindProvider.js +4 -2
  57. package/dist/test/testServices.d.ts +2 -0
  58. package/dist/test/testServices.js +4 -1
  59. package/dist/utils/elementRef.d.ts +1 -1
  60. package/dist/utils/elementRef.js +6 -1
  61. package/dist/utils/fqnRef.d.ts +3 -0
  62. package/dist/utils/fqnRef.js +15 -4
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.js +9 -0
  65. package/dist/utils/projectId.d.ts +2 -1
  66. package/dist/utils/projectId.js +11 -1
  67. package/dist/validation/_shared.js +2 -2
  68. package/dist/validation/deployment-checks.js +24 -10
  69. package/dist/validation/element-ref.d.ts +4 -0
  70. package/dist/validation/element-ref.js +12 -0
  71. package/dist/validation/element.d.ts +1 -1
  72. package/dist/validation/element.js +1 -1
  73. package/dist/validation/imports.d.ts +5 -0
  74. package/dist/validation/imports.js +30 -0
  75. package/dist/validation/index.d.ts +1 -1
  76. package/dist/validation/index.js +47 -45
  77. package/dist/validation/relation.d.ts +2 -2
  78. package/dist/validation/relation.js +24 -27
  79. package/dist/validation/specification.d.ts +9 -9
  80. package/dist/validation/specification.js +9 -9
  81. package/dist/validation/view-predicates/{element-with.d.ts → fqn-expr-with.d.ts} +1 -1
  82. package/dist/validation/view-predicates/fqn-expr-with.js +42 -0
  83. package/dist/validation/view-predicates/fqn-ref-expr.d.ts +4 -0
  84. package/dist/validation/view-predicates/fqn-ref-expr.js +53 -0
  85. package/dist/validation/view-predicates/incoming.d.ts +1 -1
  86. package/dist/validation/view-predicates/incoming.js +2 -2
  87. package/dist/validation/view-predicates/index.d.ts +6 -6
  88. package/dist/validation/view-predicates/index.js +6 -6
  89. package/dist/validation/view-predicates/outgoing.d.ts +1 -1
  90. package/dist/validation/view-predicates/outgoing.js +8 -4
  91. package/dist/validation/view-predicates/{expanded-element.d.ts → relation-expr.d.ts} +1 -1
  92. package/dist/validation/view-predicates/relation-expr.js +39 -0
  93. package/dist/validation/view-predicates/relation-with.d.ts +1 -1
  94. package/dist/validation/view-predicates/relation-with.js +8 -5
  95. package/dist/views/likec4-views.d.ts +1 -0
  96. package/dist/views/likec4-views.js +23 -4
  97. package/dist/workspace/AstNodeDescriptionProvider.d.ts +1 -1
  98. package/dist/workspace/AstNodeDescriptionProvider.js +2 -3
  99. package/dist/workspace/IndexManager.d.ts +1 -1
  100. package/dist/workspace/IndexManager.js +5 -4
  101. package/dist/workspace/LangiumDocuments.d.ts +1 -1
  102. package/dist/workspace/LangiumDocuments.js +3 -5
  103. package/dist/workspace/ProjectsManager.d.ts +25 -7
  104. package/dist/workspace/ProjectsManager.js +76 -32
  105. package/dist/workspace/WorkspaceManager.d.ts +4 -5
  106. package/dist/workspace/WorkspaceManager.js +53 -20
  107. package/package.json +17 -13
  108. package/dist/validation/dynamic-view-rule.d.ts +0 -4
  109. package/dist/validation/dynamic-view-rule.js +0 -17
  110. package/dist/validation/view-predicates/element-with.js +0 -31
  111. package/dist/validation/view-predicates/expanded-element.js +0 -12
  112. package/dist/validation/view-predicates/expression-v2.d.ts +0 -5
  113. package/dist/validation/view-predicates/expression-v2.js +0 -83
@@ -25,6 +25,7 @@ export declare class DefaultLikeC4Views implements LikeC4Views {
25
25
  private cache;
26
26
  private viewsWithReportedErrors;
27
27
  private ModelBuilder;
28
+ private queue;
28
29
  constructor(services: LikeC4Services);
29
30
  get layouter(): GraphvizLayouter;
30
31
  computedViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<ComputedView[]>;
@@ -1,9 +1,9 @@
1
1
  import { loggable } from "@likec4/log";
2
+ import PQueue from "p-queue";
2
3
  import prettyMs from "pretty-ms";
3
4
  import { values } from "remeda";
4
5
  import { CancellationToken } from "vscode-jsonrpc";
5
6
  import { logError, logger as rootLogger, logWarnError } from "../logger.js";
6
- const logger = rootLogger.getChild("Views");
7
7
  export class DefaultLikeC4Views {
8
8
  constructor(services) {
9
9
  this.services = services;
@@ -12,6 +12,7 @@ export class DefaultLikeC4Views {
12
12
  cache = /* @__PURE__ */ new WeakMap();
13
13
  viewsWithReportedErrors = /* @__PURE__ */ new Set();
14
14
  ModelBuilder;
15
+ queue = new PQueue({ concurrency: 4, timeout: 1e4, throwOnTimeout: true });
15
16
  get layouter() {
16
17
  return this.services.likec4.Layouter;
17
18
  }
@@ -24,19 +25,28 @@ export class DefaultLikeC4Views {
24
25
  if (views.length === 0) {
25
26
  return [];
26
27
  }
28
+ const logger = rootLogger.getChild(["views", projectId ?? ""]);
27
29
  logger.debug`layoutAll: ${views.length} views`;
28
30
  const results = [];
29
31
  const tasks = [];
30
32
  for (const view of views) {
31
33
  this.viewsWithReportedErrors.delete(view.id);
32
34
  tasks.push(
33
- this.layouter.layout(view).then((result) => {
35
+ Promise.resolve().then(async () => {
36
+ const result = await this.queue.add(async () => {
37
+ logger.debug`layouting view ${view.id}...`;
38
+ return await this.layouter.layout(view);
39
+ });
40
+ if (!result) {
41
+ return Promise.reject(new Error(`Failed to layout view ${view.id}`));
42
+ }
43
+ logger.debug`done layout view ${view.id}`;
34
44
  this.viewsWithReportedErrors.delete(view.id);
35
45
  this.cache.set(view, result);
36
46
  return result;
37
47
  }).catch((e) => {
48
+ logger.warn(`fail layout view ${view.id}`, { e });
38
49
  this.cache.delete(view);
39
- logWarnError(e);
40
50
  return Promise.reject(e);
41
51
  })
42
52
  );
@@ -44,6 +54,8 @@ export class DefaultLikeC4Views {
44
54
  for (const task of await Promise.allSettled(tasks)) {
45
55
  if (task.status === "fulfilled") {
46
56
  results.push(task.value);
57
+ } else {
58
+ logger.error(loggable(task.reason));
47
59
  }
48
60
  }
49
61
  if (results.length !== views.length) {
@@ -56,6 +68,7 @@ export class DefaultLikeC4Views {
56
68
  async layoutView(viewId, projectId, cancelToken = CancellationToken.None) {
57
69
  const model = await this.ModelBuilder.buildLikeC4Model(projectId, cancelToken);
58
70
  const view = model.findView(viewId)?.$view;
71
+ const logger = rootLogger.getChild(["views", projectId ?? ""]);
59
72
  if (!view) {
60
73
  logger.warn`layoutView ${viewId} not found`;
61
74
  return null;
@@ -67,7 +80,13 @@ export class DefaultLikeC4Views {
67
80
  }
68
81
  try {
69
82
  const start = performance.now();
70
- const result = await this.layouter.layout(view);
83
+ const result = await this.queue.add(async () => {
84
+ logger.debug`layouting view ${view.id}...`;
85
+ return await this.layouter.layout(view);
86
+ });
87
+ if (!result) {
88
+ throw new Error(`Failed to layout view ${viewId}`);
89
+ }
71
90
  this.viewsWithReportedErrors.delete(viewId);
72
91
  this.cache.set(view, result);
73
92
  logger.debug(`layout {viewId} ready in ${prettyMs(performance.now() - start)}`, { viewId });
@@ -1,7 +1,7 @@
1
1
  import { type AstNode, type AstNodeDescription, type LangiumDocument, DefaultAstNodeDescriptionProvider } from 'langium';
2
2
  import type { LikeC4Services } from '../module';
3
3
  export declare class AstNodeDescriptionProvider extends DefaultAstNodeDescriptionProvider {
4
- private projects;
4
+ protected services: LikeC4Services;
5
5
  constructor(services: LikeC4Services);
6
6
  createDescription(node: AstNode, name: string | undefined, document?: LangiumDocument): AstNodeDescription;
7
7
  }
@@ -3,15 +3,14 @@ import {
3
3
  DefaultAstNodeDescriptionProvider
4
4
  } from "langium";
5
5
  export class AstNodeDescriptionProvider extends DefaultAstNodeDescriptionProvider {
6
- projects;
7
6
  constructor(services) {
8
7
  super(services);
9
- this.projects = services.shared.workspace.ProjectsManager;
8
+ this.services = services;
10
9
  }
11
10
  createDescription(node, name, document) {
12
11
  const doc = document ?? AstUtils.getDocument(node);
13
12
  const description = super.createDescription(node, name, document);
14
- doc.likec4ProjectId ??= this.projects.belongsTo(doc.uri);
13
+ doc.likec4ProjectId ??= this.services.shared.workspace.ProjectsManager.belongsTo(doc.uri);
15
14
  description.likec4ProjectId = doc.likec4ProjectId;
16
15
  return description;
17
16
  }
@@ -3,7 +3,7 @@ import { type AstNodeDescription, type LangiumDocument, type Stream, DefaultInde
3
3
  import { CancellationToken } from 'vscode-jsonrpc';
4
4
  import type { LikeC4SharedServices } from '../module';
5
5
  export declare class IndexManager extends DefaultIndexManager {
6
- private projects;
6
+ protected services: LikeC4SharedServices;
7
7
  constructor(services: LikeC4SharedServices);
8
8
  updateContent(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void>;
9
9
  projectElements(projectId: ProjectId, nodeType?: string, uris?: Set<string>): Stream<AstNodeDescription>;
@@ -1,17 +1,18 @@
1
1
  import { DefaultIndexManager, stream } from "langium";
2
2
  import { CancellationToken } from "vscode-jsonrpc";
3
3
  export class IndexManager extends DefaultIndexManager {
4
- projects;
5
4
  constructor(services) {
6
5
  super(services);
7
- this.projects = services.workspace.ProjectsManager;
6
+ this.services = services;
8
7
  }
9
8
  async updateContent(document, cancelToken = CancellationToken.None) {
10
- document.likec4ProjectId = this.projects.belongsTo(document.uri);
9
+ const projects = this.services.workspace.ProjectsManager;
10
+ document.likec4ProjectId = projects.belongsTo(document.uri);
11
11
  await super.updateContent(document, cancelToken);
12
12
  }
13
13
  projectElements(projectId, nodeType, uris) {
14
+ const projects = this.services.workspace.ProjectsManager;
14
15
  let documentUris = stream(this.symbolIndex.keys());
15
- return documentUris.filter((uri) => this.projects.belongsTo(uri) === projectId && (!uris || uris.has(uri))).flatMap((uri) => this.getFileDescriptions(uri, nodeType));
16
+ return documentUris.filter((uri) => projects.belongsTo(uri) === projectId && (!uris || uris.has(uri))).flatMap((uri) => this.getFileDescriptions(uri, nodeType));
16
17
  }
17
18
  }
@@ -3,7 +3,7 @@ import { type Stream, DefaultLangiumDocuments } from 'langium';
3
3
  import { type LikeC4LangiumDocument } from '../ast';
4
4
  import type { LikeC4SharedServices } from '../module';
5
5
  export declare class LangiumDocuments extends DefaultLangiumDocuments {
6
- private projects;
6
+ protected services: LikeC4SharedServices;
7
7
  constructor(services: LikeC4SharedServices);
8
8
  /**
9
9
  * Returns all user documents, excluding built-in documents.
@@ -3,22 +3,20 @@ import { groupBy, prop } from "remeda";
3
3
  import { isLikeC4LangiumDocument } from "../ast.js";
4
4
  import { isLikeC4Builtin } from "../likec4lib.js";
5
5
  export class LangiumDocuments extends DefaultLangiumDocuments {
6
- projects;
7
6
  constructor(services) {
8
7
  super(services);
9
- this.projects = services.workspace.ProjectsManager;
8
+ this.services = services;
10
9
  }
11
10
  /**
12
11
  * Returns all user documents, excluding built-in documents.
13
12
  */
14
13
  get allExcludingBuiltin() {
14
+ const projects = this.services.workspace.ProjectsManager;
15
15
  return super.all.filter((doc) => {
16
16
  if (!isLikeC4LangiumDocument(doc) || isLikeC4Builtin(doc.uri)) {
17
17
  return false;
18
18
  }
19
- if (!doc.likec4ProjectId) {
20
- doc.likec4ProjectId = this.projects.belongsTo(doc.uri);
21
- }
19
+ doc.likec4ProjectId ??= projects.belongsTo(doc.uri);
22
20
  return true;
23
21
  });
24
22
  }
@@ -1,7 +1,14 @@
1
1
  import type { NonEmptyReadonlyArray, ProjectId } from '@likec4/core';
2
2
  import { type FileSystemNode, type LangiumDocument, URI, WorkspaceCache } from 'langium';
3
+ import picomatch from 'picomatch/posix';
3
4
  import { ProjectConfig } from '../config';
4
5
  import type { LikeC4SharedServices } from '../module';
6
+ interface Project {
7
+ id: ProjectId;
8
+ config: ProjectConfig;
9
+ folder: string;
10
+ exclude?: picomatch.Matcher;
11
+ }
5
12
  export declare class ProjectsManager {
6
13
  protected services: LikeC4SharedServices;
7
14
  /**
@@ -14,35 +21,46 @@ export declare class ProjectsManager {
14
21
  * The mapping between project config files and project IDs.
15
22
  */
16
23
  private projectIdToFolder;
17
- private _mappingsToProject;
18
24
  /**
19
25
  * Registered projects.
20
26
  * Sorted descending by the number of segments in the folder path.
21
27
  * This ensures that the most specific project is used for a document.
22
28
  */
23
29
  private _projects;
30
+ private defaultGlobalProject;
24
31
  constructor(services: LikeC4SharedServices);
32
+ /**
33
+ * Returns:
34
+ * - the default project ID if there are no projects.
35
+ * - the ID of the only project
36
+ * - undefined if there are multiple projects.
37
+ */
25
38
  get defaultProjectId(): ProjectId | undefined;
26
39
  get all(): NonEmptyReadonlyArray<ProjectId>;
27
- getProject(projectId: ProjectId): {
40
+ getProject(arg: ProjectId | LangiumDocument): {
41
+ id: ProjectId;
28
42
  folder: URI;
29
43
  config: Readonly<ProjectConfig>;
30
44
  };
31
45
  ensureProjectId(projectId?: ProjectId | undefined): ProjectId;
32
46
  hasMultipleProjects(): boolean;
47
+ checkIfExcluded(documentUri: URI): boolean;
48
+ isConfigFile(entry: FileSystemNode): boolean;
33
49
  /**
34
50
  * Checks if the provided file system entry is a valid project config file.
35
51
  *
36
52
  * @param entry The file system entry to check
37
53
  * @returns {boolean} Returns true if the entry is a valid config file, false otherwise.
38
54
  */
39
- loadConfigFile(entry: FileSystemNode): Promise<boolean>;
40
- registerProject(configFile: URI): Promise<void>;
55
+ loadConfigFile(entry: FileSystemNode): Promise<Project | undefined>;
56
+ registerProject(configFile: URI): Promise<Project>;
41
57
  registerProject(opts: {
42
58
  config: ProjectConfig;
43
59
  folderUri: URI | string;
44
- }): Promise<void>;
60
+ }): Promise<Project>;
45
61
  belongsTo(document: LangiumDocument | URI | string): ProjectId;
46
- private getProjectId;
47
- protected get mappingsToProject(): WorkspaceCache<string, ProjectId>;
62
+ protected findProjectForDocument(documentUri: string): Omit<Project, 'folder'>;
63
+ private _mappingsToProject;
64
+ protected get mappingsToProject(): WorkspaceCache<string, Omit<Project, 'folder'>>;
48
65
  }
66
+ export {};
@@ -1,7 +1,14 @@
1
1
  import { BiMap, invariant, nonNullable } from "@likec4/core";
2
2
  import { URI, WorkspaceCache } from "langium";
3
- import { hasAtLeast, map, pipe, prop, sortBy } from "remeda";
4
- import { hasProtocol, joinRelativeURL, parseFilename, withoutProtocol, withProtocol } from "ufo";
3
+ import picomatch from "picomatch/posix";
4
+ import { hasAtLeast, isNullish, map, pipe, prop, sortBy } from "remeda";
5
+ import {
6
+ hasProtocol,
7
+ joinRelativeURL,
8
+ parseFilename,
9
+ withoutProtocol,
10
+ withProtocol
11
+ } from "ufo";
5
12
  import { parseConfigJson } from "../config/index.js";
6
13
  import { logger as mainLogger } from "../logger.js";
7
14
  const logger = mainLogger.getChild("ProjectsManager");
@@ -24,14 +31,26 @@ export class ProjectsManager {
24
31
  * The mapping between project config files and project IDs.
25
32
  */
26
33
  projectIdToFolder = new BiMap();
27
- // The mapping between document URIs and their corresponding project IDs.
28
- _mappingsToProject;
29
34
  /**
30
35
  * Registered projects.
31
36
  * Sorted descending by the number of segments in the folder path.
32
37
  * This ensures that the most specific project is used for a document.
33
38
  */
34
39
  _projects = [];
40
+ defaultGlobalProject = {
41
+ id: ProjectsManager.DefaultProjectId,
42
+ config: {
43
+ name: ProjectsManager.DefaultProjectId,
44
+ exclude: ["**/node_modules/**/*"]
45
+ },
46
+ exclude: picomatch("**/node_modules/**/*")
47
+ };
48
+ /**
49
+ * Returns:
50
+ * - the default project ID if there are no projects.
51
+ * - the ID of the only project
52
+ * - undefined if there are multiple projects.
53
+ */
35
54
  get defaultProjectId() {
36
55
  if (this._projects.length > 1) {
37
56
  return void 0;
@@ -47,20 +66,24 @@ export class ProjectsManager {
47
66
  }
48
67
  return [ProjectsManager.DefaultProjectId];
49
68
  }
50
- getProject(projectId) {
51
- if (projectId === ProjectsManager.DefaultProjectId) {
52
- const folder = this.services.workspace.WorkspaceManager.workspaceUri;
69
+ getProject(arg) {
70
+ const id = typeof arg === "string" ? arg : arg.likec4ProjectId || this.belongsTo(arg);
71
+ if (id === ProjectsManager.DefaultProjectId) {
72
+ const folder2 = this.services.workspace.WorkspaceManager.workspaceUri;
53
73
  return {
54
- folder,
55
- config: {
56
- name: ProjectsManager.DefaultProjectId
57
- }
74
+ id,
75
+ folder: folder2,
76
+ config: this.defaultGlobalProject.config
58
77
  };
59
78
  }
60
- const project = nonNullable(this._projects.find(({ id }) => id === projectId), `Project "${projectId}" not found`);
79
+ const {
80
+ config,
81
+ folder
82
+ } = nonNullable(this._projects.find((p) => p.id === id), `Project "${id}" not found`);
61
83
  return {
62
- folder: URI.parse(project.folder),
63
- config: project.config
84
+ id,
85
+ folder: URI.parse(folder),
86
+ config
64
87
  };
65
88
  }
66
89
  ensureProjectId(projectId) {
@@ -79,6 +102,15 @@ export class ProjectsManager {
79
102
  hasMultipleProjects() {
80
103
  return this._projects.length > 1;
81
104
  }
105
+ checkIfExcluded(documentUri) {
106
+ let docUriAsString = documentUri.toString();
107
+ const project = this.findProjectForDocument(docUriAsString);
108
+ return project.exclude ? project.exclude(withoutProtocol(docUriAsString)) : false;
109
+ }
110
+ isConfigFile(entry) {
111
+ const filename = parseFilename(entry.uri.toString(), { strict: false })?.toLowerCase();
112
+ return !!filename && ProjectsManager.ConfigFileNames.includes(filename);
113
+ }
82
114
  /**
83
115
  * Checks if the provided file system entry is a valid project config file.
84
116
  *
@@ -87,17 +119,12 @@ export class ProjectsManager {
87
119
  */
88
120
  async loadConfigFile(entry) {
89
121
  if (entry.isDirectory) {
90
- return false;
91
- }
92
- const filename = parseFilename(entry.uri.fsPath, { strict: false });
93
- if (!filename) {
94
- return false;
122
+ return void 0;
95
123
  }
96
- if (ProjectsManager.ConfigFileNames.includes(filename)) {
97
- await this.registerProject(entry.uri);
98
- return true;
124
+ if (this.isConfigFile(entry)) {
125
+ return await this.registerProject(entry.uri);
99
126
  }
100
- return false;
127
+ return void 0;
101
128
  }
102
129
  async registerProject(opts) {
103
130
  if (URI.isUri(opts)) {
@@ -109,9 +136,10 @@ export class ProjectsManager {
109
136
  return this.registerProject({ config: config2, folderUri: folderUri2 });
110
137
  }
111
138
  const { config, folderUri } = opts;
112
- const id = config.name;
113
- if (this._projects.some(({ id: existingId }) => existingId === id)) {
114
- throw new Error(`Project ID ${id} already registered`);
139
+ let id = config.name;
140
+ let i = 1;
141
+ while (this.projectIdToFolder.has(id)) {
142
+ id = `${config.name}-${i++}`;
115
143
  }
116
144
  let folder;
117
145
  if (URI.isUri(folderUri)) {
@@ -119,14 +147,25 @@ export class ProjectsManager {
119
147
  } else {
120
148
  folder = hasProtocol(folderUri) ? folderUri : withProtocol(folderUri, "file://");
121
149
  }
150
+ const project = {
151
+ id,
152
+ config,
153
+ folder
154
+ };
155
+ if (isNullish(config.exclude)) {
156
+ project.exclude = this.defaultGlobalProject.exclude;
157
+ } else if (hasAtLeast(config.exclude, 1)) {
158
+ project.exclude = picomatch(config.exclude);
159
+ }
122
160
  this._projects = pipe(
123
- [...this._projects, { folder, config, id }],
161
+ [...this._projects, project],
124
162
  sortBy(
125
163
  [({ folder: folder2 }) => withoutProtocol(folder2).split("/").length, "desc"]
126
164
  )
127
165
  );
128
166
  this.projectIdToFolder.set(id, folder);
129
- logger.debug`registered project ${id} folder: ${folder})`;
167
+ logger.info`register project ${id} folder: ${folder})`;
168
+ return project;
130
169
  }
131
170
  belongsTo(document) {
132
171
  let documentUri;
@@ -137,12 +176,17 @@ export class ProjectsManager {
137
176
  } else {
138
177
  documentUri = document.uri.toString();
139
178
  }
140
- return this.mappingsToProject.get(documentUri, () => this.getProjectId(documentUri));
179
+ return this.findProjectForDocument(documentUri).id;
141
180
  }
142
- getProjectId(documentUri) {
143
- const project = this._projects.find(({ folder }) => documentUri.toString().startsWith(folder));
144
- return project?.id ?? ProjectsManager.DefaultProjectId;
181
+ findProjectForDocument(documentUri) {
182
+ return this.mappingsToProject.get(documentUri, () => {
183
+ const project = this._projects.find(({ folder }) => documentUri.startsWith(folder));
184
+ return project ?? this.defaultGlobalProject;
185
+ });
145
186
  }
187
+ // The mapping between document URIs and their corresponding project ID
188
+ // Lazy-created due to initialization order of the LanguageServer
189
+ _mappingsToProject;
146
190
  get mappingsToProject() {
147
191
  this._mappingsToProject ??= new WorkspaceCache(this.services);
148
192
  return this._mappingsToProject;
@@ -1,11 +1,11 @@
1
- import type { LangiumDocument } from 'langium';
1
+ import type { FileSystemNode, LangiumDocument } from 'langium';
2
2
  import { DefaultWorkspaceManager } from 'langium';
3
3
  import type { WorkspaceFolder } from 'vscode-languageserver';
4
4
  import { URI } from 'vscode-uri';
5
5
  import type { LikeC4SharedServices } from '../module';
6
6
  export declare class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
7
+ private services;
7
8
  private documentFactory;
8
- private projects;
9
9
  constructor(services: LikeC4SharedServices);
10
10
  /**
11
11
  * Load all additional documents that shall be visible in the context of the given workspace
@@ -14,10 +14,9 @@ export declare class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
14
14
  */
15
15
  protected loadAdditionalDocuments(folders: WorkspaceFolder[], collector: (document: LangiumDocument) => void): Promise<void>;
16
16
  /**
17
- * We override the default implementation to process project config files during the traversal.
18
- * This is necessary to ensure that the project config files are loaded and processed correctly.
17
+ * Determine whether the given folder entry shall be included while indexing the workspace.
19
18
  */
20
- protected traverseFolder(workspaceFolder: WorkspaceFolder, folderPath: URI, fileExtensions: string[], collector: (document: LangiumDocument) => void): Promise<void>;
19
+ protected includeEntry(_workspaceFolder: WorkspaceFolder, entry: FileSystemNode, fileExtensions: string[]): boolean;
21
20
  workspace(): any;
22
21
  get workspaceUri(): URI;
23
22
  get workspaceURL(): URL;
@@ -2,42 +2,75 @@ import { hasAtLeast, invariant } from "@likec4/core";
2
2
  import { DefaultWorkspaceManager } from "langium";
3
3
  import { URI } from "vscode-uri";
4
4
  import * as BuiltIn from "../likec4lib.js";
5
+ import { logError } from "../logger.js";
5
6
  export class LikeC4WorkspaceManager extends DefaultWorkspaceManager {
6
- documentFactory;
7
- projects;
8
7
  constructor(services) {
9
8
  super(services);
9
+ this.services = services;
10
10
  this.documentFactory = services.workspace.LangiumDocumentFactory;
11
- this.projects = services.workspace.ProjectsManager;
12
11
  }
12
+ documentFactory;
13
13
  /**
14
14
  * Load all additional documents that shall be visible in the context of the given workspace
15
15
  * folders and add them to the collector. This can be used to include built-in libraries of
16
16
  * your language, which can be either loaded from provided files or constructed in memory.
17
17
  */
18
18
  async loadAdditionalDocuments(folders, collector) {
19
+ const projects = this.services.workspace.ProjectsManager;
20
+ for (const folder of folders) {
21
+ try {
22
+ const content = await this.fileSystemProvider.readDirectory(URI.parse(folder.uri));
23
+ for (const entry of content) {
24
+ try {
25
+ await projects.loadConfigFile(entry);
26
+ } catch (error) {
27
+ logError(error);
28
+ }
29
+ }
30
+ } catch (error) {
31
+ logError(error);
32
+ }
33
+ }
19
34
  collector(this.documentFactory.fromString(BuiltIn.Content, URI.parse(BuiltIn.Uri)));
20
35
  await super.loadAdditionalDocuments(folders, collector);
21
36
  }
37
+ // /**
38
+ // * We override the default implementation to process project config files during the traversal.
39
+ // * This is necessary to ensure that the project config files are loaded and processed correctly.
40
+ // */
41
+ // protected override async traverseFolder(
42
+ // workspaceFolder: WorkspaceFolder,
43
+ // folderPath: URI,
44
+ // fileExtensions: string[],
45
+ // collector: (document: LangiumDocument) => void,
46
+ // ): Promise<void> {
47
+ // // Then load other files
48
+ // for (const entry of nonConfigFiles) {
49
+ // try {
50
+ // if (this.includeEntry(workspaceFolder, entry, fileExtensions)) {
51
+ // if (entry.isDirectory) {
52
+ // await this.traverseFolder(workspaceFolder, entry.uri, fileExtensions, collector)
53
+ // } else if (entry.isFile) {
54
+ // const document = await this.langiumDocuments.getOrCreateDocument(entry.uri)
55
+ // collector(document)
56
+ // }
57
+ // }
58
+ // } catch (error) {
59
+ // logError(error)
60
+ // }
61
+ // }
62
+ // }
22
63
  /**
23
- * We override the default implementation to process project config files during the traversal.
24
- * This is necessary to ensure that the project config files are loaded and processed correctly.
64
+ * Determine whether the given folder entry shall be included while indexing the workspace.
25
65
  */
26
- async traverseFolder(workspaceFolder, folderPath, fileExtensions, collector) {
27
- const content = await this.fileSystemProvider.readDirectory(folderPath);
28
- await Promise.all(content.map(async (entry) => {
29
- if (await this.projects.loadConfigFile(entry)) {
30
- return;
31
- }
32
- if (this.includeEntry(workspaceFolder, entry, fileExtensions)) {
33
- if (entry.isDirectory) {
34
- await this.traverseFolder(workspaceFolder, entry.uri, fileExtensions, collector);
35
- } else if (entry.isFile) {
36
- const document = await this.langiumDocuments.getOrCreateDocument(entry.uri);
37
- collector(document);
38
- }
39
- }
40
- }));
66
+ includeEntry(_workspaceFolder, entry, fileExtensions) {
67
+ if (this.services.workspace.ProjectsManager.isConfigFile(entry)) {
68
+ return false;
69
+ }
70
+ if (entry.isFile) {
71
+ return !this.services.workspace.ProjectsManager.checkIfExcluded(entry.uri);
72
+ }
73
+ return super.includeEntry(_workspaceFolder, entry, fileExtensions);
41
74
  }
42
75
  workspace() {
43
76
  if (this.folders && hasAtLeast(this.folders, 1)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@likec4/language-server",
3
3
  "description": "LikeC4 Language Server",
4
- "version": "1.27.3",
4
+ "version": "1.28.1",
5
5
  "license": "MIT",
6
6
  "bugs": "https://github.com/likec4/likec4/issues",
7
7
  "homepage": "https://likec4.dev",
@@ -96,7 +96,8 @@
96
96
  "devDependencies": {
97
97
  "@msgpack/msgpack": "^3.1.1",
98
98
  "@smithy/util-base64": "^4.0.0",
99
- "@types/node": "^20.17.28",
99
+ "@types/node": "^20.17.30",
100
+ "@types/picomatch": "^4.0.0",
100
101
  "@types/which": "^3.0.4",
101
102
  "esm-env": "^1.2.2",
102
103
  "fast-equals": "^5.2.2",
@@ -107,26 +108,29 @@
107
108
  "langium-cli": "3.4.0",
108
109
  "natural-compare-lite": "^1.4.0",
109
110
  "p-debounce": "^4.0.0",
111
+ "p-queue": "^8.1.0",
112
+ "picomatch": "^4.0.2",
113
+ "p-timeout": "^6.1.4",
110
114
  "pretty-ms": "^9.2.0",
111
115
  "remeda": "^2.21.2",
112
116
  "strip-indent": "^4.0.0",
113
117
  "tsx": "~4.19.3",
114
- "turbo": "^2.4.4",
115
- "type-fest": "^4.38.0",
116
- "typescript": "^5.8.2",
117
- "ufo": "^1.5.4",
118
+ "turbo": "^2.5.0",
119
+ "type-fest": "^4.39.1",
120
+ "typescript": "^5.8.3",
121
+ "ufo": "^1.6.1",
118
122
  "unbuild": "^3.5.0",
119
123
  "valibot": "^1.0.0",
120
- "vitest": "^3.0.9",
124
+ "vitest": "^3.1.1",
121
125
  "vscode-languageserver": "9.0.1",
122
- "vscode-languageserver-types": "3.17.5",
123
126
  "vscode-languageserver-protocol": "3.17.5",
127
+ "vscode-languageserver-types": "3.17.5",
124
128
  "which": "^5.0.0",
125
- "@likec4/core": "1.27.3",
126
- "@likec4/icons": "1.27.3",
127
- "@likec4/layouts": "1.27.3",
128
- "@likec4/log": "1.27.3",
129
- "@likec4/tsconfig": "1.27.3"
129
+ "@likec4/core": "1.28.1",
130
+ "@likec4/layouts": "1.28.1",
131
+ "@likec4/tsconfig": "1.28.1",
132
+ "@likec4/log": "1.28.1",
133
+ "@likec4/icons": "1.28.1"
130
134
  },
131
135
  "scripts": {
132
136
  "typecheck": "tsc --noEmit",
@@ -1,4 +0,0 @@
1
- import type { ValidationCheck } from 'langium';
2
- import { ast } from '../ast';
3
- import type { LikeC4Services } from '../module';
4
- export declare const dynamicViewRulePredicate: (_services: LikeC4Services) => ValidationCheck<ast.DynamicViewPredicateIterator>;
@@ -1,17 +0,0 @@
1
- import { ast, elementExpressionFromPredicate } from "../ast.js";
2
- import { tryOrLog } from "./_shared.js";
3
- export const dynamicViewRulePredicate = (_services) => {
4
- return tryOrLog((predicate, accept) => {
5
- const expr = elementExpressionFromPredicate(predicate.value);
6
- switch (true) {
7
- case ast.isElementKindExpression(expr):
8
- case ast.isElementTagExpression(expr):
9
- case ast.isWildcardExpression(expr): {
10
- accept("warning", `Predicate is ignored, as not supported in dynamic views`, {
11
- node: predicate
12
- });
13
- return;
14
- }
15
- }
16
- });
17
- };