@likec4/language-server 1.25.1 → 1.26.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 (60) hide show
  1. package/dist/LikeC4FileSystem.js +6 -1
  2. package/dist/LikeC4LanguageServices.d.ts +77 -0
  3. package/dist/LikeC4LanguageServices.js +118 -0
  4. package/dist/Rpc.js +106 -28
  5. package/dist/ast.d.ts +10 -0
  6. package/dist/bundled.mjs +2352 -2278
  7. package/dist/config/index.d.ts +1 -0
  8. package/dist/config/index.js +1 -0
  9. package/dist/config/schema.d.ts +8 -0
  10. package/dist/config/schema.js +19 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/lsp/CodeLensProvider.js +3 -1
  13. package/dist/model/builder/buildModel.d.ts +8 -1
  14. package/dist/model/deployments-index.js +1 -1
  15. package/dist/model/fqn-index.d.ts +9 -6
  16. package/dist/model/fqn-index.js +24 -14
  17. package/dist/model/model-builder.d.ts +16 -6
  18. package/dist/model/model-builder.js +64 -50
  19. package/dist/model/model-locator.d.ts +11 -10
  20. package/dist/model/model-locator.js +32 -14
  21. package/dist/model/model-parser.d.ts +148 -148
  22. package/dist/model/model-parser.js +2 -2
  23. package/dist/model/parser/ModelParser.js +14 -2
  24. package/dist/model-change/ModelChanges.d.ts +1 -1
  25. package/dist/model-change/ModelChanges.js +2 -2
  26. package/dist/module.d.ts +9 -3
  27. package/dist/module.js +19 -5
  28. package/dist/protocol.d.ts +98 -16
  29. package/dist/protocol.js +21 -6
  30. package/dist/references/scope-provider.d.ts +24 -11
  31. package/dist/references/scope-provider.js +97 -68
  32. package/dist/shared/index.d.ts +0 -1
  33. package/dist/shared/index.js +0 -1
  34. package/dist/test/testServices.d.ts +15 -1
  35. package/dist/test/testServices.js +65 -17
  36. package/dist/utils/index.d.ts +3 -0
  37. package/dist/utils/index.js +3 -0
  38. package/dist/utils/projectId.d.ts +3 -0
  39. package/dist/utils/projectId.js +6 -0
  40. package/dist/validation/deployment-checks.js +5 -2
  41. package/dist/validation/element.js +2 -1
  42. package/dist/validation/specification.js +14 -7
  43. package/dist/validation/view.js +10 -8
  44. package/dist/views/index.d.ts +1 -1
  45. package/dist/views/index.js +1 -1
  46. package/dist/views/likec4-views.d.ts +17 -8
  47. package/dist/views/likec4-views.js +12 -11
  48. package/dist/workspace/AstNodeDescriptionProvider.d.ts +7 -0
  49. package/dist/workspace/AstNodeDescriptionProvider.js +18 -0
  50. package/dist/workspace/IndexManager.d.ts +10 -0
  51. package/dist/workspace/IndexManager.js +17 -0
  52. package/dist/workspace/LangiumDocuments.d.ts +14 -0
  53. package/dist/workspace/LangiumDocuments.js +31 -0
  54. package/dist/workspace/ProjectsManager.d.ts +48 -0
  55. package/dist/workspace/ProjectsManager.js +150 -0
  56. package/dist/{shared → workspace}/WorkspaceManager.d.ts +8 -2
  57. package/dist/{shared → workspace}/WorkspaceManager.js +22 -0
  58. package/dist/workspace/index.d.ts +5 -0
  59. package/dist/workspace/index.js +5 -0
  60. package/package.json +17 -8
@@ -0,0 +1 @@
1
+ export { parseConfigJson, ProjectConfig, validateConfig } from './schema';
@@ -0,0 +1 @@
1
+ export { parseConfigJson, ProjectConfig, validateConfig } from "./schema.js";
@@ -0,0 +1,8 @@
1
+ import * as v from 'valibot';
2
+ export declare const ProjectConfig: v.ObjectSchema<{
3
+ readonly name: v.SchemaWithPipe<[v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>, v.DescriptionAction<string, "Project name, must be unique in the workspace">]>;
4
+ readonly exclude: v.SchemaWithPipe<[v.ExactOptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, v.DescriptionAction<string[], "List of file patterns to exclude from the project, default is [\"node_modules\"]">]>;
5
+ }, undefined>;
6
+ export type ProjectConfig = v.InferOutput<typeof ProjectConfig>;
7
+ export declare function parseConfigJson(config: string): ProjectConfig;
8
+ export declare function validateConfig(config: unknown): ProjectConfig;
@@ -0,0 +1,19 @@
1
+ import JSON5 from "json5";
2
+ import * as v from "valibot";
3
+ export const ProjectConfig = v.object({
4
+ name: v.pipe(
5
+ v.string(),
6
+ v.nonEmpty(),
7
+ v.description("Project name, must be unique in the workspace")
8
+ ),
9
+ exclude: v.pipe(
10
+ v.exactOptional(v.array(v.string())),
11
+ v.description('List of file patterns to exclude from the project, default is ["node_modules"]')
12
+ )
13
+ });
14
+ export function parseConfigJson(config) {
15
+ return validateConfig(JSON5.parse(config));
16
+ }
17
+ export function validateConfig(config) {
18
+ return v.parse(ProjectConfig, config);
19
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { LikeC4FileSystem } from './LikeC4FileSystem';
2
2
  import { type LikeC4Services, type LikeC4SharedServices } from './module';
3
3
  export { getLspConnectionSink, logger as lspLogger } from './logger';
4
4
  export type { DocumentParser, LikeC4ModelBuilder, LikeC4ModelLocator, LikeC4ModelParser } from './model';
5
+ export type { LikeC4LanguageServices } from './LikeC4LanguageServices';
5
6
  export { isLikeC4Builtin } from './likec4lib';
6
7
  export { createCustomLanguageServices, createLanguageServices, LikeC4Module } from './module';
7
8
  export type { LikeC4Services, LikeC4SharedServices } from './module';
@@ -1,6 +1,7 @@
1
1
  import { DocumentState } from "langium";
2
2
  import { isLikeC4LangiumDocument, ViewOps } from "../ast.js";
3
3
  import { logger } from "../logger.js";
4
+ import { projectIdFrom } from "../utils/index.js";
4
5
  export class LikeC4CodeLensProvider {
5
6
  constructor(services) {
6
7
  this.services = services;
@@ -21,6 +22,7 @@ export class LikeC4CodeLensProvider {
21
22
  if (!range || !viewId) {
22
23
  return [];
23
24
  }
25
+ const projectId = projectIdFrom(ast);
24
26
  return {
25
27
  range: {
26
28
  start: range.start,
@@ -31,7 +33,7 @@ export class LikeC4CodeLensProvider {
31
33
  },
32
34
  command: {
33
35
  command: "likec4.open-preview",
34
- arguments: [viewId],
36
+ arguments: [viewId, projectId],
35
37
  title: "open preview"
36
38
  }
37
39
  };
@@ -1,3 +1,10 @@
1
1
  import type * as c4 from '@likec4/core';
2
2
  import type { ParsedLikeC4LangiumDocument } from '../../ast';
3
- export declare function buildModel(docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model;
3
+ /**
4
+ * Each document was parsed into a ParsedLikeC4LangiumDocument, where elements
5
+ * do not inherit styles from specification.
6
+ *
7
+ * This function builds a model from all documents, merging the specifications
8
+ * and globals, and applying the extends to the elements.
9
+ */
10
+ export declare function buildModel(docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4ModelData;
@@ -93,6 +93,6 @@ export class DeploymentsIndex extends FqnIndex {
93
93
  logWarnError(e);
94
94
  }
95
95
  }
96
- return new DocumentFqnIndex(root, children, descendants, byfqn);
96
+ return new DocumentFqnIndex(root, children, descendants, byfqn, this.projects.belongsTo(document));
97
97
  }
98
98
  }
@@ -1,12 +1,14 @@
1
- import { type Fqn } from '@likec4/core/types';
1
+ import { type Fqn, type ProjectId } from '@likec4/core/types';
2
2
  import { DefaultWeakMap, MultiMap } from '@likec4/core/utils';
3
- import { type AstNode, type LangiumDocuments, type Stream, WorkspaceCache } from 'langium';
3
+ import { type AstNode, type Stream, WorkspaceCache } from 'langium';
4
4
  import { type AstNodeDescriptionWithFqn, type LikeC4LangiumDocument, ast } from '../ast';
5
5
  import type { LikeC4Services } from '../module';
6
6
  import { ADisposable } from '../utils';
7
+ import { type LangiumDocuments, ProjectsManager } from '../workspace';
7
8
  export declare class FqnIndex<AstNd extends AstNode = ast.Element> extends ADisposable {
8
9
  protected services: LikeC4Services;
9
10
  private cachePrefix;
11
+ protected projects: ProjectsManager;
10
12
  protected langiumDocuments: LangiumDocuments;
11
13
  protected documentCache: DefaultWeakMap<LikeC4LangiumDocument, DocumentFqnIndex>;
12
14
  protected workspaceCache: WorkspaceCache<string, AstNodeDescriptionWithFqn[]>;
@@ -14,12 +16,12 @@ export declare class FqnIndex<AstNd extends AstNode = ast.Element> extends ADisp
14
16
  private documents;
15
17
  get(document: LikeC4LangiumDocument): DocumentFqnIndex;
16
18
  getFqn(el: AstNd): Fqn;
17
- byFqn(fqn: Fqn): Stream<AstNodeDescriptionWithFqn>;
18
- directChildrenOf(parent: Fqn): Stream<AstNodeDescriptionWithFqn>;
19
+ byFqn(projectId: ProjectId, fqn: Fqn): Stream<AstNodeDescriptionWithFqn>;
20
+ directChildrenOf(projectId: ProjectId, parent: Fqn): Stream<AstNodeDescriptionWithFqn>;
19
21
  /**
20
22
  * Returns descedant elements with unique names in the scope
21
23
  */
22
- uniqueDescedants(parent: Fqn): Stream<AstNodeDescriptionWithFqn>;
24
+ uniqueDescedants(projectId: ProjectId, parent: Fqn): Stream<AstNodeDescriptionWithFqn>;
23
25
  protected createDocumentIndex(document: LikeC4LangiumDocument): DocumentFqnIndex;
24
26
  }
25
27
  export declare class DocumentFqnIndex {
@@ -36,6 +38,7 @@ export declare class DocumentFqnIndex {
36
38
  * All elements by FQN
37
39
  */
38
40
  private _byfqn;
41
+ readonly projectId: ProjectId;
39
42
  static readonly EMPTY: DocumentFqnIndex;
40
43
  constructor(_rootElements: Array<AstNodeDescriptionWithFqn>,
41
44
  /**
@@ -49,7 +52,7 @@ export declare class DocumentFqnIndex {
49
52
  /**
50
53
  * All elements by FQN
51
54
  */
52
- _byfqn: MultiMap<Fqn, AstNodeDescriptionWithFqn>);
55
+ _byfqn: MultiMap<Fqn, AstNodeDescriptionWithFqn>, projectId: ProjectId);
53
56
  rootElements(): readonly AstNodeDescriptionWithFqn[];
54
57
  byFqn(fqn: Fqn): readonly AstNodeDescriptionWithFqn[];
55
58
  children(parent: Fqn): readonly AstNodeDescriptionWithFqn[];
@@ -16,12 +16,14 @@ import {
16
16
  import { logWarnError } from "../logger.js";
17
17
  import { ADisposable } from "../utils/index.js";
18
18
  import { readStrictFqn } from "../utils/elementRef.js";
19
+ import { ProjectsManager } from "../workspace/index.js";
19
20
  export class FqnIndex extends ADisposable {
20
21
  constructor(services, cachePrefix = "fqn-index") {
21
22
  super();
22
23
  this.services = services;
23
24
  this.cachePrefix = cachePrefix;
24
25
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
26
+ this.projects = services.shared.workspace.ProjectsManager;
25
27
  this.documentCache = new DefaultWeakMap((doc) => this.createDocumentIndex(doc));
26
28
  this.workspaceCache = new WorkspaceCache(services.shared, DocumentState.IndexedContent);
27
29
  this.onDispose(
@@ -36,11 +38,12 @@ export class FqnIndex extends ADisposable {
36
38
  )
37
39
  );
38
40
  }
41
+ projects;
39
42
  langiumDocuments;
40
43
  documentCache;
41
44
  workspaceCache;
42
- documents() {
43
- return this.langiumDocuments.all.filter(
45
+ documents(projectId) {
46
+ return this.langiumDocuments.projectDocuments(projectId).filter(
44
47
  (d) => isLikeC4LangiumDocument(d) && d.state >= DocumentState.IndexedContent
45
48
  );
46
49
  }
@@ -62,17 +65,17 @@ export class FqnIndex extends ADisposable {
62
65
  this.get(doc);
63
66
  return nonNullable(ElementOps.readId(el), "Element fqn must be set, invalid state");
64
67
  }
65
- byFqn(fqn) {
66
- return stream(this.workspaceCache.get(`${this.cachePrefix}:${fqn}`, () => {
67
- return this.documents().toArray().flatMap((doc) => {
68
+ byFqn(projectId, fqn) {
69
+ return stream(this.workspaceCache.get(`${this.cachePrefix}:${projectId}:${fqn}`, () => {
70
+ return this.documents(projectId).toArray().flatMap((doc) => {
68
71
  return this.get(doc).byFqn(fqn);
69
72
  });
70
73
  }));
71
74
  }
72
- directChildrenOf(parent) {
75
+ directChildrenOf(projectId, parent) {
73
76
  return stream(
74
- this.workspaceCache.get(`${this.cachePrefix}:directChildrenOf:${parent}`, () => {
75
- const allchildren = this.documents().reduce((map, doc) => {
77
+ this.workspaceCache.get(`${this.cachePrefix}:${projectId}:directChildrenOf:${parent}`, () => {
78
+ const allchildren = this.documents(projectId).reduce((map, doc) => {
76
79
  this.get(doc).children(parent).forEach((desc) => {
77
80
  map.set(desc.name, desc);
78
81
  });
@@ -85,10 +88,10 @@ export class FqnIndex extends ADisposable {
85
88
  /**
86
89
  * Returns descedant elements with unique names in the scope
87
90
  */
88
- uniqueDescedants(parent) {
91
+ uniqueDescedants(projectId, parent) {
89
92
  return stream(
90
- this.workspaceCache.get(`${this.cachePrefix}:uniqueDescedants:${parent}`, () => {
91
- const { children, descendants } = this.documents().reduce((map, doc) => {
93
+ this.workspaceCache.get(`${this.cachePrefix}:${projectId}:uniqueDescedants:${parent}`, () => {
94
+ const { children, descendants } = this.documents(projectId).reduce((map, doc) => {
92
95
  const docIndex = this.get(doc);
93
96
  docIndex.children(parent).forEach((desc) => {
94
97
  map.children.set(desc.name, desc);
@@ -181,20 +184,27 @@ export class FqnIndex extends ADisposable {
181
184
  logWarnError(e);
182
185
  }
183
186
  }
184
- return new DocumentFqnIndex(root, children, descendants, byfqn);
187
+ return new DocumentFqnIndex(root, children, descendants, byfqn, this.projects.belongsTo(document));
185
188
  }
186
189
  }
187
190
  function uniqueByName(multimap) {
188
191
  return [...multimap.associations()].flatMap(([_name, descs]) => descs.length === 1 ? descs : []);
189
192
  }
190
193
  export class DocumentFqnIndex {
191
- constructor(_rootElements, _children, _descendants, _byfqn) {
194
+ constructor(_rootElements, _children, _descendants, _byfqn, projectId) {
192
195
  this._rootElements = _rootElements;
193
196
  this._children = _children;
194
197
  this._descendants = _descendants;
195
198
  this._byfqn = _byfqn;
199
+ this.projectId = projectId;
196
200
  }
197
- static EMPTY = new DocumentFqnIndex([], new MultiMap(), new MultiMap(), new MultiMap());
201
+ static EMPTY = new DocumentFqnIndex(
202
+ [],
203
+ new MultiMap(),
204
+ new MultiMap(),
205
+ new MultiMap(),
206
+ ProjectsManager.DefaultProjectId
207
+ );
198
208
  rootElements() {
199
209
  return this._rootElements;
200
210
  }
@@ -1,15 +1,25 @@
1
1
  import type * as c4 from '@likec4/core';
2
2
  import { type ViewId, LikeC4Model } from '@likec4/core';
3
- import { type Cancellation, type URI, Disposable } from 'langium';
3
+ import { type URI, Disposable } from 'langium';
4
+ import { CancellationToken } from 'vscode-jsonrpc';
4
5
  import type { LikeC4Services } from '../module';
5
6
  import { ADisposable } from '../utils';
6
7
  type ModelParsedListener = (docs: URI[]) => void;
7
- export declare class LikeC4ModelBuilder extends ADisposable {
8
+ export interface LikeC4ModelBuilder {
9
+ parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ParsedLikeC4ModelData | null>;
10
+ unsafeSyncBuildModel(projectId: c4.ProjectId): LikeC4Model.Computed;
11
+ buildLikeC4Model(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<LikeC4Model.Computed>;
12
+ computeView(viewId: ViewId, projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ComputedView | null>;
13
+ onModelParsed(callback: ModelParsedListener): Disposable;
14
+ }
15
+ export declare class DefaultLikeC4ModelBuilder extends ADisposable implements LikeC4ModelBuilder {
16
+ private projects;
8
17
  private parser;
9
18
  private listeners;
10
19
  private cache;
11
20
  private DocumentBuilder;
12
21
  private LangiumDocuments;
22
+ private mutex;
13
23
  constructor(services: LikeC4Services);
14
24
  /**
15
25
  * WARNING:
@@ -17,16 +27,16 @@ export declare class LikeC4ModelBuilder extends ADisposable {
17
27
  * Otherwise, the model may be incomplete.
18
28
  */
19
29
  private unsafeSyncParseModel;
20
- parseModel(cancelToken?: Cancellation.CancellationToken): Promise<c4.ParsedLikeC4Model | null>;
30
+ parseModel(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ParsedLikeC4ModelData | null>;
21
31
  private previousViews;
22
32
  /**
23
33
  * WARNING:
24
34
  * This method is internal and should to be called only when all documents are known to be parsed.
25
35
  * Otherwise, the model may be incomplete.
26
36
  */
27
- unsafeSyncBuildModel(): LikeC4Model.Computed;
28
- buildLikeC4Model(cancelToken?: Cancellation.CancellationToken): Promise<LikeC4Model.Computed>;
29
- computeView(viewId: ViewId, cancelToken?: Cancellation.CancellationToken): Promise<c4.ComputedView | null>;
37
+ unsafeSyncBuildModel(projectId: c4.ProjectId): LikeC4Model.Computed;
38
+ buildLikeC4Model(projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<LikeC4Model.Computed>;
39
+ computeView(viewId: ViewId, projectId?: c4.ProjectId | undefined, cancelToken?: CancellationToken): Promise<c4.ComputedView | null>;
30
40
  onModelParsed(callback: ModelParsedListener): Disposable;
31
41
  private documents;
32
42
  private notifyListeners;
@@ -6,7 +6,8 @@ import { loggable } from "@likec4/log";
6
6
  import { deepEqual as eq } from "fast-equals";
7
7
  import {
8
8
  Disposable,
9
- DocumentState
9
+ DocumentState,
10
+ interruptAndCheck
10
11
  } from "langium";
11
12
  import prettyMs from "pretty-ms";
12
13
  import {
@@ -18,26 +19,31 @@ import {
18
19
  prop,
19
20
  values
20
21
  } from "remeda";
22
+ import { CancellationToken } from "vscode-jsonrpc";
21
23
  import { isLikeC4Builtin } from "../likec4lib.js";
22
24
  import { logger as mainLogger, logWarnError } from "../logger.js";
23
25
  import { ADisposable } from "../utils/index.js";
24
26
  import { assignNavigateTo } from "../view-utils/index.js";
25
27
  import { buildModel } from "./builder/buildModel.js";
26
- const CACHE_KEY_PARSED_MODEL = "ParsedLikeC4Model";
27
- const CACHE_KEY_COMPUTED_MODEL = "ComputedLikeC4Model";
28
+ const parsedModelCacheKey = (projectId) => `parsed-model-${projectId}`;
29
+ const computedModelCacheKey = (projectId) => `computed-model-${projectId}`;
28
30
  const logger = mainLogger.getChild("ModelBuilder");
29
- export class LikeC4ModelBuilder extends ADisposable {
31
+ export class DefaultLikeC4ModelBuilder extends ADisposable {
32
+ projects;
30
33
  parser;
31
34
  listeners = [];
32
35
  cache;
33
36
  DocumentBuilder;
34
37
  LangiumDocuments;
38
+ mutex;
35
39
  constructor(services) {
36
40
  super();
41
+ this.projects = services.shared.workspace.ProjectsManager;
37
42
  this.parser = services.likec4.ModelParser;
38
43
  this.cache = services.ValidatedWorkspaceCache;
39
44
  this.DocumentBuilder = services.shared.workspace.DocumentBuilder;
40
45
  this.LangiumDocuments = services.shared.workspace.LangiumDocuments;
46
+ this.mutex = services.shared.workspace.WorkspaceLock;
41
47
  this.onDispose(
42
48
  this.DocumentBuilder.onUpdate((_changed, deleted) => {
43
49
  if (deleted.length > 0) {
@@ -63,33 +69,38 @@ export class LikeC4ModelBuilder extends ADisposable {
63
69
  * This method is internal and should to be called only when all documents are known to be parsed.
64
70
  * Otherwise, the model may be incomplete.
65
71
  */
66
- unsafeSyncParseModel() {
67
- const docs = this.documents();
68
- if (docs.length === 0) {
69
- logger.debug("no documents to build model from");
70
- return null;
71
- }
72
+ unsafeSyncParseModel(projectId) {
72
73
  const cache = this.cache;
73
- return cache.get(CACHE_KEY_PARSED_MODEL, () => {
74
+ const log = logger.getChild(["project", projectId]);
75
+ if (cache.has(parsedModelCacheKey(projectId))) {
76
+ log.debug("unsafeSyncParseModel from cache");
77
+ }
78
+ return cache.get(parsedModelCacheKey(projectId), () => {
79
+ const docs = this.documents(projectId);
80
+ if (docs.length === 0) {
81
+ logger.debug(`no documents to build model - project ${projectId}`);
82
+ return null;
83
+ }
84
+ log.debug("unsafeSyncParseModel from documents");
74
85
  return buildModel(docs);
75
86
  });
76
87
  }
77
- async parseModel(cancelToken) {
88
+ async parseModel(projectId, cancelToken = CancellationToken.None) {
89
+ const project = this.projects.ensureProjectId(projectId);
90
+ const log = logger.getChild(["project", project]);
78
91
  const cache = this.cache;
79
- const cached = cache.get(CACHE_KEY_PARSED_MODEL);
92
+ const cached = cache.get(parsedModelCacheKey(project));
80
93
  if (cached) {
81
- logger.debug("parseModel from cache");
94
+ log.debug("parseModel from cache");
82
95
  return cached;
83
96
  }
84
- if (this.LangiumDocuments.all.some((doc) => doc.state < DocumentState.Validated)) {
85
- logger.debug("parseModel: waiting for documents to be validated");
86
- await this.DocumentBuilder.waitUntil(DocumentState.Validated, cancelToken);
87
- logger.debug("parseModel: documents are validated");
88
- }
89
97
  const t0 = performance.now();
90
- const result = this.unsafeSyncParseModel();
91
- logger.debug(`parseModel in ${prettyMs(performance.now() - t0)}`);
92
- return result;
98
+ return await this.mutex.read(async () => {
99
+ await interruptAndCheck(cancelToken);
100
+ const result = this.unsafeSyncParseModel(project);
101
+ log.debug(`parseModel in ${prettyMs(performance.now() - t0)}`);
102
+ return result;
103
+ });
93
104
  }
94
105
  previousViews = {};
95
106
  /**
@@ -97,11 +108,11 @@ export class LikeC4ModelBuilder extends ADisposable {
97
108
  * This method is internal and should to be called only when all documents are known to be parsed.
98
109
  * Otherwise, the model may be incomplete.
99
110
  */
100
- unsafeSyncBuildModel() {
111
+ unsafeSyncBuildModel(projectId) {
101
112
  const cache = this.cache;
102
113
  const viewsCache = this.cache;
103
- return cache.get(CACHE_KEY_COMPUTED_MODEL, () => {
104
- const parsed = this.unsafeSyncParseModel();
114
+ return cache.get(computedModelCacheKey(projectId), () => {
115
+ const parsed = this.unsafeSyncParseModel(projectId);
105
116
  if (!parsed) {
106
117
  return LikeC4Model.EMPTY;
107
118
  }
@@ -121,52 +132,55 @@ export class LikeC4ModelBuilder extends ADisposable {
121
132
  }
122
133
  assignNavigateTo(allViews);
123
134
  const views = mapToObj(allViews, (v) => {
124
- const previous = this.previousViews[v.id];
135
+ const key = computedViewKey(projectId, v.id);
136
+ const previous = this.previousViews[key];
125
137
  const view = previous && eq(v, previous) ? previous : v;
126
- viewsCache.set(computedViewKey(v.id), view);
138
+ viewsCache.set(key, view);
127
139
  return [v.id, view];
128
140
  });
129
- this.previousViews = { ...views };
141
+ this.previousViews = { ...this.previousViews, ...views };
130
142
  return LikeC4Model.create({
131
143
  ...model,
132
144
  views
133
145
  });
134
146
  });
135
147
  }
136
- async buildLikeC4Model(cancelToken) {
148
+ async buildLikeC4Model(projectId, cancelToken = CancellationToken.None) {
149
+ const project = this.projects.ensureProjectId(projectId);
150
+ const log = logger.getChild(["project", project]);
137
151
  const cache = this.cache;
138
- const cached = cache.get(CACHE_KEY_COMPUTED_MODEL);
152
+ const cached = cache.get(computedModelCacheKey(project));
139
153
  if (cached) {
140
- logger.debug("buildLikeC4Model from cache");
154
+ log.debug("buildLikeC4Model from cache");
141
155
  return cached;
142
156
  }
143
157
  const t0 = performance.now();
144
- const model = await this.parseModel(cancelToken);
145
- if (!model) {
146
- logger.debug("buildLikeC4Model: no model");
147
- return LikeC4Model.EMPTY;
148
- }
149
- const result = this.unsafeSyncBuildModel();
150
- logger.debug(`buildLikeC4Model in ${prettyMs(performance.now() - t0)}`);
151
- return result;
158
+ return await this.mutex.read(async () => {
159
+ await interruptAndCheck(cancelToken);
160
+ const result = this.unsafeSyncBuildModel(project);
161
+ log.debug(`buildLikeC4Model in ${prettyMs(performance.now() - t0)}`);
162
+ return result;
163
+ });
152
164
  }
153
- async computeView(viewId, cancelToken) {
165
+ async computeView(viewId, projectId, cancelToken = CancellationToken.None) {
166
+ const project = this.projects.ensureProjectId(projectId);
167
+ const log = logger.getChild(["project", project]);
154
168
  const cache = this.cache;
155
- const cacheKey = computedViewKey(viewId);
169
+ const cacheKey = computedViewKey(project, viewId);
156
170
  if (cache.has(cacheKey)) {
157
171
  return cache.get(cacheKey);
158
172
  }
159
- const parsed = await this.parseModel(cancelToken);
173
+ const parsed = await this.parseModel(project, cancelToken);
160
174
  if (!parsed) {
161
175
  return null;
162
176
  }
163
177
  return cache.get(cacheKey, () => {
164
178
  const view = parsed.views[viewId];
165
179
  if (!view) {
166
- logger.warn`computeView: cant find view ${viewId}`;
180
+ log.warn`computeView: cant find view ${viewId}`;
167
181
  return null;
168
182
  }
169
- logger.debug`computeView: ${viewId}`;
183
+ log.debug`computeView: ${viewId}`;
170
184
  const computeView = LikeC4Model.makeCompute(parsed);
171
185
  const result = computeView(view);
172
186
  if (!result.isSuccess) {
@@ -189,11 +203,11 @@ export class LikeC4ModelBuilder extends ADisposable {
189
203
  }
190
204
  }
191
205
  }
192
- const previous = this.previousViews[viewId];
206
+ const previous = this.previousViews[cacheKey];
193
207
  if (previous && eq(computedView, previous)) {
194
208
  computedView = previous;
195
209
  } else {
196
- this.previousViews[viewId] = computedView;
210
+ this.previousViews[cacheKey] = computedView;
197
211
  }
198
212
  return computedView;
199
213
  });
@@ -207,8 +221,8 @@ export class LikeC4ModelBuilder extends ADisposable {
207
221
  }
208
222
  });
209
223
  }
210
- documents() {
211
- return this.parser.documents().toArray();
224
+ documents(projectId) {
225
+ return this.parser.documents(projectId).toArray();
212
226
  }
213
227
  notifyListeners(docs) {
214
228
  for (const listener of this.listeners) {
@@ -220,6 +234,6 @@ export class LikeC4ModelBuilder extends ADisposable {
220
234
  }
221
235
  }
222
236
  }
223
- function computedViewKey(viewId) {
224
- return `computed-view-${viewId}`;
237
+ function computedViewKey(projectId, viewId) {
238
+ return `computed-view-${projectId}-${viewId}`;
225
239
  }
@@ -1,6 +1,6 @@
1
1
  import type * as c4 from '@likec4/core';
2
2
  import type { Location } from 'vscode-languageserver-types';
3
- import type { ParsedAstElement } from '../ast';
3
+ import type { ParsedAstElement, ParsedAstView, ParsedLikeC4LangiumDocument } from '../ast';
4
4
  import { ast } from '../ast';
5
5
  import type { LikeC4Services } from '../module';
6
6
  export declare class LikeC4ModelLocator {
@@ -9,16 +9,17 @@ export declare class LikeC4ModelLocator {
9
9
  private deploymentsIndex;
10
10
  private langiumDocuments;
11
11
  private parser;
12
+ private projects;
12
13
  constructor(services: LikeC4Services);
13
14
  private documents;
14
- getParsedElement(astNodeOrFqn: ast.Element | c4.Fqn): ParsedAstElement | null;
15
- locateElement(fqn: c4.Fqn, _prop?: string): Location | null;
16
- locateDeploymentElement(fqn: c4.Fqn, _prop?: string): Location | null;
17
- locateRelation(relationId: c4.RelationId): Location | null;
18
- locateViewAst(viewId: c4.ViewId): {
19
- doc: Stream<import("../ast").ParsedLikeC4LangiumDocument>;
20
- view: any;
15
+ getParsedElement(...args: [ast.Element] | [c4.Fqn] | [c4.Fqn, c4.ProjectId]): ParsedAstElement | null;
16
+ locateElement(fqn: c4.Fqn, projectId?: c4.ProjectId | undefined): Location | null;
17
+ locateDeploymentElement(fqn: c4.Fqn, projectId?: c4.ProjectId | undefined): Location | null;
18
+ locateRelation(relationId: c4.RelationId, projectId?: c4.ProjectId): Location | null;
19
+ locateViewAst(viewId: c4.ViewId, projectId?: c4.ProjectId | undefined): null | {
20
+ doc: ParsedLikeC4LangiumDocument;
21
+ view: ParsedAstView;
21
22
  viewAst: ast.LikeC4View;
22
- } | null;
23
- locateView(viewId: c4.ViewId): Location | null;
23
+ };
24
+ locateView(viewId: c4.ViewId, projectId?: c4.ProjectId): Location | null;
24
25
  }
@@ -1,6 +1,7 @@
1
1
  import { AstUtils, GrammarUtils } from "langium";
2
2
  import { isString } from "remeda";
3
3
  import { ast } from "../ast.js";
4
+ import { projectIdFrom } from "../utils/index.js";
4
5
  const { findNodeForKeyword, findNodeForProperty } = GrammarUtils;
5
6
  const { getDocument } = AstUtils;
6
7
  export class LikeC4ModelLocator {
@@ -10,18 +11,31 @@ export class LikeC4ModelLocator {
10
11
  this.deploymentsIndex = services.likec4.DeploymentsIndex;
11
12
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
12
13
  this.parser = services.likec4.ModelParser;
14
+ this.projects = services.shared.workspace.ProjectsManager;
13
15
  }
14
16
  fqnIndex;
15
17
  deploymentsIndex;
16
18
  langiumDocuments;
17
19
  parser;
18
- documents() {
19
- return this.parser.documents();
20
+ projects;
21
+ documents(projectId) {
22
+ return this.parser.documents(projectId);
20
23
  }
21
- getParsedElement(astNodeOrFqn) {
24
+ // public getParsedElement(astNodeOrFqn: ast.Element): ParsedAstElement | null
25
+ // public getParsedElement(astNodeOrFqn: c4.Fqn, projectId?: c4.ProjectId): ParsedAstElement | null
26
+ getParsedElement(...args) {
27
+ let astNodeOrFqn;
28
+ let projectId;
29
+ if (args.length === 2) {
30
+ astNodeOrFqn = args[0];
31
+ projectId = args[1];
32
+ } else {
33
+ astNodeOrFqn = args[0];
34
+ projectId = isString(astNodeOrFqn) ? this.projects.ensureProjectId() : projectIdFrom(astNodeOrFqn);
35
+ }
22
36
  if (isString(astNodeOrFqn)) {
23
37
  const fqn2 = astNodeOrFqn;
24
- const entry = this.fqnIndex.byFqn(astNodeOrFqn).head();
38
+ const entry = this.fqnIndex.byFqn(projectId, astNodeOrFqn).head();
25
39
  if (!entry) {
26
40
  return null;
27
41
  }
@@ -35,8 +49,9 @@ export class LikeC4ModelLocator {
35
49
  const doc = this.parser.parse(getDocument(astNodeOrFqn));
36
50
  return doc.c4Elements.find((e) => e.id === fqn) ?? null;
37
51
  }
38
- locateElement(fqn, _prop) {
39
- const entry = this.fqnIndex.byFqn(fqn).head();
52
+ locateElement(fqn, projectId) {
53
+ const _projectId = this.projects.ensureProjectId(projectId);
54
+ const entry = this.fqnIndex.byFqn(_projectId, fqn).head();
40
55
  const docsegment = entry?.nameSegment ?? entry?.selectionSegment;
41
56
  if (!entry || !docsegment) {
42
57
  return null;
@@ -46,8 +61,9 @@ export class LikeC4ModelLocator {
46
61
  range: docsegment.range
47
62
  };
48
63
  }
49
- locateDeploymentElement(fqn, _prop) {
50
- const entry = this.deploymentsIndex.byFqn(fqn).head();
64
+ locateDeploymentElement(fqn, projectId) {
65
+ const _projectId = this.projects.ensureProjectId(projectId);
66
+ const entry = this.deploymentsIndex.byFqn(_projectId, fqn).head();
51
67
  const docsegment = entry?.nameSegment ?? entry?.selectionSegment;
52
68
  if (!entry || !docsegment) {
53
69
  return null;
@@ -57,8 +73,9 @@ export class LikeC4ModelLocator {
57
73
  range: docsegment.range
58
74
  };
59
75
  }
60
- locateRelation(relationId) {
61
- for (const doc of this.documents()) {
76
+ locateRelation(relationId, projectId) {
77
+ const project = this.projects.ensureProjectId(projectId);
78
+ for (const doc of this.documents(project)) {
62
79
  const relation = doc.c4Relations.find((r) => r.id === relationId) ?? doc.c4DeploymentRelations.find((r) => r.id === relationId);
63
80
  if (!relation) {
64
81
  continue;
@@ -84,8 +101,9 @@ export class LikeC4ModelLocator {
84
101
  }
85
102
  return null;
86
103
  }
87
- locateViewAst(viewId) {
88
- for (const doc of this.documents()) {
104
+ locateViewAst(viewId, projectId) {
105
+ const project = this.projects.ensureProjectId(projectId);
106
+ for (const doc of this.documents(project)) {
89
107
  const view = doc.c4Views.find((r) => r.id === viewId);
90
108
  if (!view) {
91
109
  continue;
@@ -104,8 +122,8 @@ export class LikeC4ModelLocator {
104
122
  }
105
123
  return null;
106
124
  }
107
- locateView(viewId) {
108
- const res = this.locateViewAst(viewId);
125
+ locateView(viewId, projectId) {
126
+ const res = this.locateViewAst(viewId, projectId);
109
127
  if (!res) {
110
128
  return null;
111
129
  }