@likec4/language-server 1.24.0 → 1.24.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.
@@ -9,14 +9,15 @@ export class LikeC4CodeLensProvider {
9
9
  if (!isLikeC4LangiumDocument(doc)) {
10
10
  return;
11
11
  }
12
- if (doc.state !== DocumentState.Validated) {
13
- logger.debug(`Waiting for document ${doc.uri.path} to be validated`);
12
+ if (doc.state <= DocumentState.Linked) {
13
+ logger.debug(`Waiting for document ${doc.uri.path} to be Linked`);
14
14
  await this.services.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated, doc.uri, cancelToken);
15
15
  logger.debug(`Document ${doc.uri.path} is validated`);
16
16
  }
17
17
  if (cancelToken) {
18
18
  await interruptAndCheck(cancelToken);
19
19
  }
20
+ this.services.likec4.ModelParser.parse(doc);
20
21
  const views = doc.parseResult.value.views.flatMap((v) => v.views);
21
22
  return views.flatMap((ast) => {
22
23
  const viewId = ViewOps.readId(ast);
@@ -1,8 +1,7 @@
1
- import { AstUtils, DocumentState, GrammarUtils, interruptAndCheck } from "langium";
1
+ import { AstUtils, GrammarUtils } from "langium";
2
2
  import { hasLeadingSlash, hasProtocol, isRelative, withoutBase, withoutLeadingSlash } from "ufo";
3
3
  import { ast, isLikeC4LangiumDocument } from "../ast.js";
4
- import { logger, logWarnError } from "../logger.js";
5
- const log = logger.getChild("DocumentLinkProvider");
4
+ import { logWarnError } from "../logger.js";
6
5
  export class LikeC4DocumentLinkProvider {
7
6
  constructor(services) {
8
7
  this.services = services;
@@ -11,19 +10,11 @@ export class LikeC4DocumentLinkProvider {
11
10
  if (!isLikeC4LangiumDocument(doc)) {
12
11
  return [];
13
12
  }
14
- if (doc.state !== DocumentState.Validated) {
15
- log.debug(`Waiting for document ${doc.uri.path} to be validated`);
16
- await this.services.shared.workspace.DocumentBuilder.waitUntil(DocumentState.Validated, doc.uri, cancelToken);
17
- log.debug(`Document ${doc.uri.path} is validated`);
18
- }
19
- if (cancelToken) {
20
- await interruptAndCheck(cancelToken);
21
- }
22
13
  return AstUtils.streamAllContents(doc.parseResult.value).filter(ast.isLinkProperty).map((n) => {
23
14
  try {
24
15
  const range = GrammarUtils.findNodeForProperty(n.$cstNode, "value")?.range;
25
- const target = this.resolveLink(doc, n.value);
26
- if (range && hasProtocol(target)) {
16
+ const target = range && this.resolveLink(doc, n.value);
17
+ if (target && hasProtocol(target)) {
27
18
  return {
28
19
  range,
29
20
  target
@@ -1,11 +1,10 @@
1
- import { type LikeC4LangiumDocument } from '../ast';
1
+ import { type LikeC4LangiumDocument, ast } from '../ast';
2
2
  import type { LikeC4Services } from '../module';
3
3
  import type { LikeC4NameProvider } from '../references';
4
4
  import { DocumentFqnIndex, FqnIndex } from './fqn-index';
5
- export declare class DeploymentsIndex extends FqnIndex {
5
+ export declare class DeploymentsIndex extends FqnIndex<ast.DeploymentElement> {
6
6
  protected services: LikeC4Services;
7
7
  protected Names: LikeC4NameProvider;
8
- protected cachePrefix: string;
9
8
  constructor(services: LikeC4Services);
10
9
  protected createDocumentIndex(document: LikeC4LangiumDocument): DocumentFqnIndex;
11
10
  }
@@ -1,5 +1,5 @@
1
1
  import { ancestorsFqn, AsFqn } from "@likec4/core";
2
- import { MultiMap } from "langium";
2
+ import { MultiMap } from "@likec4/core/utils";
3
3
  import { isDefined, isTruthy } from "remeda";
4
4
  import {
5
5
  ast,
@@ -10,12 +10,11 @@ import { readStrictFqn } from "../utils/elementRef.js";
10
10
  import { DocumentFqnIndex, FqnIndex } from "./fqn-index.js";
11
11
  export class DeploymentsIndex extends FqnIndex {
12
12
  constructor(services) {
13
- super(services);
13
+ super(services, "deployments-index");
14
14
  this.services = services;
15
15
  this.Names = services.references.NameProvider;
16
16
  }
17
17
  Names;
18
- cachePrefix = "deployments-index";
19
18
  createDocumentIndex(document) {
20
19
  const rootNodes = document.parseResult.value.deployments.flatMap((m) => m.elements);
21
20
  if (rootNodes.length === 0) {
@@ -33,7 +32,7 @@ export class DeploymentsIndex extends FqnIndex {
33
32
  id: fqn
34
33
  };
35
34
  ElementOps.writeId(node, fqn);
36
- byfqn.add(fqn, desc);
35
+ byfqn.set(fqn, desc);
37
36
  return desc;
38
37
  };
39
38
  const traverseNode = (node, parentFqn) => {
@@ -50,7 +49,7 @@ export class DeploymentsIndex extends FqnIndex {
50
49
  if (!parentFqn) {
51
50
  root.push(desc);
52
51
  } else {
53
- children.add(parentFqn, desc);
52
+ children.set(parentFqn, desc);
54
53
  }
55
54
  if (ast.isDeployedInstance(node)) {
56
55
  return [];
@@ -68,18 +67,21 @@ export class DeploymentsIndex extends FqnIndex {
68
67
  }
69
68
  }
70
69
  }
71
- const directChildren = children.get(thisFqn);
72
70
  _nested = [
73
- ...directChildren,
71
+ ...children.get(thisFqn) ?? [],
74
72
  ..._nested
75
73
  ];
76
- descendants.addAll(thisFqn, _nested);
74
+ for (const child of _nested) {
75
+ descendants.set(thisFqn, child);
76
+ }
77
77
  if (ast.isExtendDeployment(node)) {
78
- ancestorsFqn(thisFqn).forEach((ancestor) => {
79
- descendants.addAll(ancestor, _nested);
80
- });
78
+ for (const ancestor of ancestorsFqn(thisFqn)) {
79
+ for (const child of _nested) {
80
+ descendants.set(ancestor, child);
81
+ }
82
+ }
81
83
  }
82
- return descendants.get(thisFqn);
84
+ return descendants.get(thisFqn) ?? [];
83
85
  };
84
86
  for (const node of rootNodes) {
85
87
  try {
@@ -1,18 +1,19 @@
1
1
  import { type Fqn } from '@likec4/core/types';
2
- import { DefaultWeakMap } from '@likec4/core/utils';
3
- import { type LangiumDocuments, type Stream, MultiMap } from 'langium';
2
+ import { DefaultWeakMap, MultiMap } from '@likec4/core/utils';
3
+ import { type AstNode, type LangiumDocuments, 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
- export declare class FqnIndex extends ADisposable {
7
+ export declare class FqnIndex<AstNd extends AstNode = ast.Element> extends ADisposable {
8
8
  protected services: LikeC4Services;
9
+ private cachePrefix;
9
10
  protected langiumDocuments: LangiumDocuments;
10
11
  protected documentCache: DefaultWeakMap<LikeC4LangiumDocument, DocumentFqnIndex>;
11
- protected cachePrefix: string;
12
- constructor(services: LikeC4Services);
12
+ protected workspaceCache: WorkspaceCache<string, AstNodeDescriptionWithFqn[]>;
13
+ constructor(services: LikeC4Services, cachePrefix?: string);
13
14
  private documents;
14
15
  get(document: LikeC4LangiumDocument): DocumentFqnIndex;
15
- getFqn(el: ast.Element | ast.DeploymentElement): Fqn;
16
+ getFqn(el: AstNd): Fqn;
16
17
  byFqn(fqn: Fqn): Stream<AstNodeDescriptionWithFqn>;
17
18
  directChildrenOf(parent: Fqn): Stream<AstNodeDescriptionWithFqn>;
18
19
  /**
@@ -1,11 +1,11 @@
1
1
  import { invariant, nonNullable } from "@likec4/core";
2
2
  import { AsFqn } from "@likec4/core/types";
3
- import { ancestorsFqn, compareNatural, DefaultWeakMap, sortNaturalByFqn } from "@likec4/core/utils";
3
+ import { ancestorsFqn, compareNatural, DefaultWeakMap, MultiMap, sortNaturalByFqn } from "@likec4/core/utils";
4
4
  import {
5
5
  AstUtils,
6
6
  DocumentState,
7
- MultiMap,
8
- stream
7
+ stream,
8
+ WorkspaceCache
9
9
  } from "langium";
10
10
  import { isDefined, isEmpty, isTruthy } from "remeda";
11
11
  import {
@@ -17,11 +17,13 @@ import { logWarnError } from "../logger.js";
17
17
  import { ADisposable } from "../utils/index.js";
18
18
  import { readStrictFqn } from "../utils/elementRef.js";
19
19
  export class FqnIndex extends ADisposable {
20
- constructor(services) {
20
+ constructor(services, cachePrefix = "fqn-index") {
21
21
  super();
22
22
  this.services = services;
23
+ this.cachePrefix = cachePrefix;
23
24
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
24
25
  this.documentCache = new DefaultWeakMap((doc) => this.createDocumentIndex(doc));
26
+ this.workspaceCache = new WorkspaceCache(services.shared, DocumentState.IndexedContent);
25
27
  this.onDispose(
26
28
  services.shared.workspace.DocumentBuilder.onDocumentPhase(
27
29
  DocumentState.IndexedContent,
@@ -36,7 +38,7 @@ export class FqnIndex extends ADisposable {
36
38
  }
37
39
  langiumDocuments;
38
40
  documentCache;
39
- cachePrefix = "fqn-index";
41
+ workspaceCache;
40
42
  documents() {
41
43
  return this.langiumDocuments.all.filter(
42
44
  (d) => isLikeC4LangiumDocument(d) && d.state >= DocumentState.IndexedContent
@@ -49,53 +51,64 @@ export class FqnIndex extends ADisposable {
49
51
  return this.documentCache.get(document);
50
52
  }
51
53
  getFqn(el) {
54
+ invariant(ast.isElement(el) || ast.isDeploymentElement(el));
52
55
  let id = ElementOps.readId(el);
53
56
  if (isTruthy(id)) {
54
57
  return id;
55
58
  }
56
59
  const doc = AstUtils.getDocument(el);
57
60
  invariant(isLikeC4LangiumDocument(doc));
61
+ logWarnError(`Document ${doc.uri.path} is not indexed, but getFqn was called`);
58
62
  this.get(doc);
59
63
  return nonNullable(ElementOps.readId(el), "Element fqn must be set, invalid state");
60
64
  }
61
65
  byFqn(fqn) {
62
- return this.documents().flatMap((doc) => {
63
- return this.get(doc).byFqn(fqn);
64
- });
66
+ return stream(this.workspaceCache.get(`${this.cachePrefix}:${fqn}`, () => {
67
+ return this.documents().toArray().flatMap((doc) => {
68
+ return this.get(doc).byFqn(fqn);
69
+ });
70
+ }));
65
71
  }
66
72
  directChildrenOf(parent) {
67
73
  return stream(
68
- this.documents().reduce((map, doc) => {
69
- this.get(doc).children(parent).forEach((desc) => {
70
- map.add(desc.name, desc);
71
- });
72
- return map;
73
- }, new MultiMap()).entriesGroupedByKey().flatMap(([_name, descs]) => descs.length === 1 ? descs : []).toArray().sort((a, b) => compareNatural(a.name, b.name))
74
+ this.workspaceCache.get(`${this.cachePrefix}:directChildrenOf:${parent}`, () => {
75
+ const allchildren = this.documents().reduce((map, doc) => {
76
+ this.get(doc).children(parent).forEach((desc) => {
77
+ map.set(desc.name, desc);
78
+ });
79
+ return map;
80
+ }, new MultiMap());
81
+ return uniqueByName(allchildren).sort((a, b) => compareNatural(a.name, b.name));
82
+ })
74
83
  );
75
84
  }
76
85
  /**
77
86
  * Returns descedant elements with unique names in the scope
78
87
  */
79
88
  uniqueDescedants(parent) {
80
- const { children, descendants } = this.documents().reduce((map, doc) => {
81
- const docIndex = this.get(doc);
82
- docIndex.children(parent).forEach((desc) => {
83
- map.children.add(desc.name, desc);
84
- });
85
- docIndex.descendants(parent).forEach((desc) => {
86
- map.descendants.add(desc.name, desc);
87
- });
88
- return map;
89
- }, {
90
- children: new MultiMap(),
91
- descendants: new MultiMap()
92
- });
93
- const uniqueChildren = children.entriesGroupedByKey().flatMap(([_name, descs]) => descs.length === 1 ? descs : []).toArray().sort((a, b) => compareNatural(a.name, b.name));
94
- const uniqueDescendants = descendants.entriesGroupedByKey().flatMap(([_name, descs]) => descs.length === 1 && !children.has(_name) ? descs : []).toArray();
95
- return stream([
96
- ...uniqueChildren,
97
- ...sortNaturalByFqn(uniqueDescendants)
98
- ]);
89
+ return stream(
90
+ this.workspaceCache.get(`${this.cachePrefix}:uniqueDescedants:${parent}`, () => {
91
+ const { children, descendants } = this.documents().reduce((map, doc) => {
92
+ const docIndex = this.get(doc);
93
+ docIndex.children(parent).forEach((desc) => {
94
+ map.children.set(desc.name, desc);
95
+ });
96
+ docIndex.descendants(parent).forEach((desc) => {
97
+ map.descendants.set(desc.name, desc);
98
+ });
99
+ return map;
100
+ }, {
101
+ children: new MultiMap(),
102
+ descendants: new MultiMap()
103
+ });
104
+ const uniqueChildren = uniqueByName(children).sort((a, b) => compareNatural(a.name, b.name));
105
+ const uniqueDescendants = [...descendants.associations()].flatMap(([_name, descs]) => descs.length === 1 && !children.has(_name) ? descs : []);
106
+ return [
107
+ ...uniqueChildren,
108
+ ...sortNaturalByFqn(uniqueDescendants)
109
+ ];
110
+ })
111
+ );
99
112
  }
100
113
  createDocumentIndex(document) {
101
114
  const rootElements = document.parseResult.value.models.flatMap((m) => m.elements);
@@ -113,7 +126,7 @@ export class FqnIndex extends ADisposable {
113
126
  id: fqn
114
127
  };
115
128
  ElementOps.writeId(node, fqn);
116
- byfqn.add(fqn, desc);
129
+ byfqn.set(fqn, desc);
117
130
  return desc;
118
131
  };
119
132
  const traverseNode = (el, parentFqn) => {
@@ -124,7 +137,7 @@ export class FqnIndex extends ADisposable {
124
137
  if (!parentFqn) {
125
138
  root.push(desc);
126
139
  } else {
127
- children.add(parentFqn, desc);
140
+ children.set(parentFqn, desc);
128
141
  }
129
142
  } else {
130
143
  thisFqn = readStrictFqn(el.element);
@@ -141,18 +154,22 @@ export class FqnIndex extends ADisposable {
141
154
  }
142
155
  }
143
156
  }
144
- const directChildren = children.get(thisFqn);
157
+ const directChildren = children.get(thisFqn) ?? [];
145
158
  _nested = [
146
159
  ...directChildren,
147
160
  ..._nested
148
161
  ];
149
- descendants.addAll(thisFqn, _nested);
162
+ for (const child of _nested) {
163
+ descendants.set(thisFqn, child);
164
+ }
150
165
  if (ast.isExtendElement(el)) {
151
- ancestorsFqn(thisFqn).forEach((ancestor) => {
152
- descendants.addAll(ancestor, _nested);
153
- });
166
+ for (const ancestor of ancestorsFqn(thisFqn)) {
167
+ for (const child of _nested) {
168
+ descendants.set(ancestor, child);
169
+ }
170
+ }
154
171
  }
155
- return descendants.get(thisFqn);
172
+ return descendants.get(thisFqn) ?? [];
156
173
  };
157
174
  for (const node of rootElements) {
158
175
  try {
@@ -167,6 +184,9 @@ export class FqnIndex extends ADisposable {
167
184
  return new DocumentFqnIndex(root, children, descendants, byfqn);
168
185
  }
169
186
  }
187
+ function uniqueByName(multimap) {
188
+ return [...multimap.associations()].flatMap(([_name, descs]) => descs.length === 1 ? descs : []);
189
+ }
170
190
  export class DocumentFqnIndex {
171
191
  constructor(_rootElements, _children, _descendants, _byfqn) {
172
192
  this._rootElements = _rootElements;
@@ -179,12 +199,12 @@ export class DocumentFqnIndex {
179
199
  return this._rootElements;
180
200
  }
181
201
  byFqn(fqn) {
182
- return this._byfqn.get(fqn);
202
+ return this._byfqn.get(fqn) ?? [];
183
203
  }
184
204
  children(parent) {
185
- return this._children.get(parent);
205
+ return this._children.get(parent) ?? [];
186
206
  }
187
207
  descendants(nodeName) {
188
- return this._descendants.get(nodeName);
208
+ return this._descendants.get(nodeName) ?? [];
189
209
  }
190
210
  }
@@ -4,15 +4,12 @@ import { type Cancellation, type URI, Disposable } from 'langium';
4
4
  import type { LikeC4Services } from '../module';
5
5
  import { ADisposable } from '../utils';
6
6
  type ModelParsedListener = (docs: URI[]) => void;
7
- type ParseModelResult = {
8
- model: c4.ParsedLikeC4Model;
9
- computeView: (view: c4.LikeC4View) => c4.ComputeViewResult;
10
- };
11
7
  export declare class LikeC4ModelBuilder extends ADisposable {
12
8
  private parser;
13
9
  private listeners;
14
10
  private cache;
15
11
  private DocumentBuilder;
12
+ private LangiumDocuments;
16
13
  constructor(services: LikeC4Services);
17
14
  /**
18
15
  * WARNING:
@@ -20,7 +17,7 @@ export declare class LikeC4ModelBuilder extends ADisposable {
20
17
  * Otherwise, the model may be incomplete.
21
18
  */
22
19
  private unsafeSyncParseModel;
23
- parseModel(cancelToken?: Cancellation.CancellationToken): Promise<ParseModelResult | null>;
20
+ parseModel(cancelToken?: Cancellation.CancellationToken): Promise<c4.ParsedLikeC4Model | null>;
24
21
  private previousViews;
25
22
  /**
26
23
  * WARNING:
@@ -21,17 +21,19 @@ import { assignNavigateTo } from "../view-utils/index.js";
21
21
  import { buildModel } from "./builder/buildModel.js";
22
22
  const CACHE_KEY_PARSED_MODEL = "ParsedLikeC4Model";
23
23
  const CACHE_KEY_COMPUTED_MODEL = "ComputedLikeC4Model";
24
- const logger = mainLogger.getChild("model-builder");
24
+ const logger = mainLogger.getChild("ModelBuilder");
25
25
  export class LikeC4ModelBuilder extends ADisposable {
26
26
  parser;
27
27
  listeners = [];
28
28
  cache;
29
29
  DocumentBuilder;
30
+ LangiumDocuments;
30
31
  constructor(services) {
31
32
  super();
32
33
  this.parser = services.likec4.ModelParser;
33
34
  this.cache = services.ValidatedWorkspaceCache;
34
35
  this.DocumentBuilder = services.shared.workspace.DocumentBuilder;
36
+ this.LangiumDocuments = services.shared.workspace.LangiumDocuments;
35
37
  this.onDispose(
36
38
  this.DocumentBuilder.onUpdate((_changed, deleted) => {
37
39
  if (deleted.length > 0) {
@@ -43,7 +45,6 @@ export class LikeC4ModelBuilder extends ADisposable {
43
45
  this.DocumentBuilder.onBuildPhase(
44
46
  DocumentState.Validated,
45
47
  (docs, _cancelToken) => {
46
- logger.debug("onValidated ({docslength} docs)", { docslength: docs.length });
47
48
  this.notifyListeners(docs.map((d) => d.uri));
48
49
  }
49
50
  )
@@ -64,9 +65,7 @@ export class LikeC4ModelBuilder extends ADisposable {
64
65
  const cache = this.cache;
65
66
  return cache.get(CACHE_KEY_PARSED_MODEL, () => {
66
67
  logger.debug("unsafeSyncParseModel ({docslength} docs)", { docslength: docs.length });
67
- const model = buildModel(docs);
68
- const computeView = LikeC4Model.makeCompute(model);
69
- return { model, computeView };
68
+ return buildModel(docs);
70
69
  });
71
70
  }
72
71
  async parseModel(cancelToken) {
@@ -75,7 +74,10 @@ export class LikeC4ModelBuilder extends ADisposable {
75
74
  if (cached) {
76
75
  return await Promise.resolve(cached);
77
76
  }
78
- await this.DocumentBuilder.waitUntil(DocumentState.Validated, cancelToken);
77
+ if (this.LangiumDocuments.all.some((doc) => doc.state < DocumentState.Validated)) {
78
+ logger.debug("parseModel: waiting for documents to be validated");
79
+ await this.DocumentBuilder.waitUntil(DocumentState.Validated, cancelToken);
80
+ }
79
81
  return this.unsafeSyncParseModel();
80
82
  }
81
83
  previousViews = {};
@@ -85,20 +87,18 @@ export class LikeC4ModelBuilder extends ADisposable {
85
87
  * Otherwise, the model may be incomplete.
86
88
  */
87
89
  unsafeSyncBuildModel() {
88
- const parsed = this.unsafeSyncParseModel();
89
- if (!parsed) {
90
- return LikeC4Model.EMPTY;
91
- }
92
90
  const cache = this.cache;
93
91
  const viewsCache = this.cache;
94
92
  return cache.get(CACHE_KEY_COMPUTED_MODEL, () => {
93
+ const parsed = this.unsafeSyncParseModel();
94
+ if (!parsed) {
95
+ return LikeC4Model.EMPTY;
96
+ }
95
97
  const {
96
- model: {
97
- views: parsedViews,
98
- ...model
99
- },
100
- computeView
98
+ views: parsedViews,
99
+ ...model
101
100
  } = parsed;
101
+ const computeView = LikeC4Model.makeCompute(parsed);
102
102
  const allViews = [];
103
103
  for (const view of values(parsedViews)) {
104
104
  const result = computeView(view);
@@ -145,19 +145,20 @@ export class LikeC4ModelBuilder extends ADisposable {
145
145
  return null;
146
146
  }
147
147
  return cache.get(cacheKey, () => {
148
- const view = parsed.model.views[viewId];
148
+ const view = parsed.views[viewId];
149
149
  if (!view) {
150
150
  logger.warn(`[ModelBuilder] Cannot find view ${viewId}`);
151
151
  return null;
152
152
  }
153
- const result = parsed.computeView(view);
153
+ const computeView = LikeC4Model.makeCompute(parsed);
154
+ const result = computeView(view);
154
155
  if (!result.isSuccess) {
155
156
  logWarnError(result.error);
156
157
  return null;
157
158
  }
158
159
  let computedView = result.view;
159
160
  const allElementViews = pipe(
160
- parsed.model.views,
161
+ parsed.views,
161
162
  values(),
162
163
  filter(isScopedElementView),
163
164
  filter((v) => v.id !== viewId),
@@ -16,7 +16,7 @@ export declare class LikeC4ModelLocator {
16
16
  locateDeploymentElement(fqn: c4.Fqn, _prop?: string): Location | null;
17
17
  locateRelation(relationId: c4.RelationId): Location | null;
18
18
  locateViewAst(viewId: c4.ViewId): {
19
- doc: any;
19
+ doc: Stream<import("../ast").ParsedLikeC4LangiumDocument>;
20
20
  view: any;
21
21
  viewAst: ast.LikeC4View;
22
22
  } | null;
@@ -16,7 +16,7 @@ export class LikeC4ModelLocator {
16
16
  langiumDocuments;
17
17
  parser;
18
18
  documents() {
19
- return this.parser.documents().toArray();
19
+ return this.parser.documents();
20
20
  }
21
21
  getParsedElement(astNodeOrFqn) {
22
22
  if (isString(astNodeOrFqn)) {