@likec4/language-server 0.36.0 → 0.37.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.
@@ -10,6 +10,14 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
10
10
  type: SemanticTokenTypes.keyword,
11
11
  modifier: [SemanticTokenModifiers.defaultLibrary]
12
12
  });
13
+ if ('arr' in node) {
14
+ acceptor({
15
+ node,
16
+ property: 'arr',
17
+ type: SemanticTokenTypes.keyword,
18
+ modifier: [SemanticTokenModifiers.defaultLibrary]
19
+ });
20
+ }
13
21
  if (ast.isElementRef(node) || ast.isStrictElementRef(node)) {
14
22
  acceptor({
15
23
  node,
@@ -18,21 +26,21 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
18
26
  });
19
27
  return;
20
28
  }
21
- if (ast.isWildcardExpression(node)) {
29
+ if (ast.isElementViewRef(node)) {
22
30
  acceptor({
23
31
  node,
24
- property: 'isWildcard',
32
+ property: 'view',
25
33
  type: SemanticTokenTypes.variable
26
34
  });
27
35
  return;
28
36
  }
29
- if ('arr' in node) {
37
+ if (ast.isWildcardExpression(node)) {
30
38
  acceptor({
31
39
  node,
32
- property: 'arr',
33
- type: SemanticTokenTypes.keyword,
34
- modifier: [SemanticTokenModifiers.defaultLibrary]
40
+ property: 'isWildcard',
41
+ type: SemanticTokenTypes.variable
35
42
  });
43
+ return;
36
44
  }
37
45
  // if (
38
46
  // ast.isRelation(node) ||
@@ -197,13 +205,6 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
197
205
  modifier: [SemanticTokenModifiers.declaration]
198
206
  });
199
207
  }
200
- if (node.viewOf) {
201
- acceptor({
202
- node,
203
- keyword: 'of',
204
- type: SemanticTokenTypes.keyword
205
- });
206
- }
207
208
  }
208
209
  }
209
210
  //# sourceMappingURL=SemanticTokenProvider.js.map
@@ -2,7 +2,7 @@ import { AsFqn, nonexhaustive } from '@likec4/core';
2
2
  import { MultiMap } from 'langium';
3
3
  import { isEmpty, isNil } from 'remeda';
4
4
  import { ElementOps, ast } from '../ast';
5
- import { strictElementRefFqn } from '../elementRef';
5
+ import { fqnElementRef } from '../elementRef';
6
6
  export function computeDocumentFqn(document, services) {
7
7
  const c4fqns = (document.c4fqns = new MultiMap());
8
8
  const { model } = document.parseResult.value;
@@ -19,7 +19,7 @@ export function computeDocumentFqn(document, services) {
19
19
  }
20
20
  if (ast.isExtendElement(el)) {
21
21
  if (!isNil(el.body) && !isEmpty(el.body.elements)) {
22
- const fqn = strictElementRefFqn(el.element);
22
+ const fqn = fqnElementRef(el.element);
23
23
  el.body.elements.forEach(child => traverseStack.push([child, fqn]));
24
24
  }
25
25
  continue;
@@ -51,9 +51,10 @@ export class FqnIndex {
51
51
  documents() {
52
52
  return this.langiumDocuments.all.filter(isFqnIndexedDocument);
53
53
  }
54
- entries() {
54
+ entries(filterByFqn = () => true) {
55
55
  return this.documents().flatMap(doc => doc.c4fqns
56
56
  .entries()
57
+ .filter(([fqn]) => filterByFqn(fqn))
57
58
  .map(([fqn, entry]) => {
58
59
  const el = entry.el.deref();
59
60
  if (el) {
@@ -90,8 +91,7 @@ export class FqnIndex {
90
91
  }
91
92
  directChildrenOf(parent) {
92
93
  return stream([parent]).flatMap(_parent => {
93
- const children = this.entries()
94
- .filter(e => parentFqn(e.fqn) === _parent)
94
+ const children = this.entries(fqn => parentFqn(fqn) === _parent)
95
95
  .map((entry) => [entry.name, entry])
96
96
  .toArray();
97
97
  if (children.length === 0) {
@@ -112,9 +112,7 @@ export class FqnIndex {
112
112
  const childrenNames = new Set();
113
113
  const descedants = [];
114
114
  const nested = new MultiMap();
115
- this.entries()
116
- .filter(e => e.fqn.startsWith(prefix))
117
- .forEach(e => {
115
+ this.entries(f => f.startsWith(prefix)).forEach(e => {
118
116
  const name = nameFromFqn(e.fqn);
119
117
  const entry = { ...e, name };
120
118
  // To keep direct children always
@@ -1,4 +1,4 @@
1
- import { type c4 } from '@likec4/core';
1
+ import { type ViewID, type c4 } from '@likec4/core';
2
2
  import type { LikeC4Services } from '../module';
3
3
  export declare class LikeC4ModelBuilder {
4
4
  private services;
@@ -7,7 +7,9 @@ export declare class LikeC4ModelBuilder {
7
7
  constructor(services: LikeC4Services);
8
8
  private cleanCache;
9
9
  private documents;
10
+ buildRawModel(): c4.LikeC4RawModel | null;
10
11
  buildModel(): c4.LikeC4Model | null;
12
+ computeView(viewId: ViewID): c4.ComputedView | null;
11
13
  private scheduledCb;
12
14
  private notifyClient;
13
15
  }
@@ -1,5 +1,4 @@
1
- import { ModelIndex, assignNavigateTo, compareByFqnHierarchically, computeView, parentFqn } from '@likec4/core';
2
- import { clone } from 'rambdax';
1
+ import { ModelIndex, compareByFqnHierarchically, parentFqn, resolveRulesExtendedViews, computeView, assignNavigateTo, isStrictElementView } from '@likec4/core';
3
2
  import * as R from 'remeda';
4
3
  import { isValidLikeC4LangiumDocument } from '../ast';
5
4
  import { logError, logWarnError, logger } from '../logger';
@@ -58,42 +57,49 @@ function buildModel(docs) {
58
57
  return null;
59
58
  };
60
59
  const relations = R.pipe(R.flatMap(docs, d => d.c4Relations), R.map(toModelRelation), R.compact, R.mapToObj(r => [r.id, r]));
61
- const modelIndex = ModelIndex.from({ elements, relations });
62
- const toModelView = (view) => {
63
- try {
64
- // eslint-disable-next-line prefer-const
65
- let { astPath, rules, title, description, tags, links, ...model } = view;
66
- if (!title && view.viewOf) {
67
- title = elements[view.viewOf]?.title;
68
- }
69
- if (!title && view.id === 'index') {
70
- title = 'Landscape view';
71
- }
72
- const computeResult = computeView({
73
- ...model,
74
- title: title ?? null,
75
- description: description ?? null,
76
- tags: tags ?? null,
77
- links: links ?? null,
78
- rules: clone(rules)
79
- }, modelIndex);
80
- if (!computeResult.isSuccess) {
81
- logWarnError(computeResult.error);
82
- return null;
83
- }
84
- return computeResult.view;
60
+ const toElementView = (view) => {
61
+ // eslint-disable-next-line prefer-const
62
+ let { astPath, rules, title, description, tags, links, ...model } = view;
63
+ if (!title && 'viewOf' in view) {
64
+ title = elements[view.viewOf]?.title;
85
65
  }
86
- catch (e) {
87
- logError(e);
88
- return null;
66
+ if (!title && view.id === 'index') {
67
+ title = 'Landscape view';
89
68
  }
69
+ return {
70
+ ...model,
71
+ title: title ?? null,
72
+ description: description ?? null,
73
+ tags: tags ?? null,
74
+ links: links ?? null,
75
+ rules
76
+ };
90
77
  };
91
- const views = R.pipe(R.flatMap(docs, d => d.c4Views), R.map(toModelView), R.compact);
92
- assignNavigateTo(views);
78
+ const views = R.pipe(R.flatMap(docs, d => d.c4Views), R.map(toElementView), R.compact, R.mapToObj(v => [v.id, v]));
79
+ // add index view if not present
80
+ if (!('index' in views)) {
81
+ views['index'] = {
82
+ id: 'index',
83
+ title: 'Landscape',
84
+ description: null,
85
+ tags: null,
86
+ links: null,
87
+ rules: [
88
+ {
89
+ isInclude: true,
90
+ exprs: [
91
+ {
92
+ wildcard: true
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ };
98
+ }
93
99
  return {
94
100
  elements,
95
101
  relations,
96
- views: R.mapToObj(views, v => [v.id, v])
102
+ views: resolveRulesExtendedViews(views)
97
103
  };
98
104
  }
99
105
  export class LikeC4ModelBuilder {
@@ -114,7 +120,7 @@ export class LikeC4ModelBuilder {
114
120
  documents() {
115
121
  return this.langiumDocuments.all.filter(isValidLikeC4LangiumDocument).toArray();
116
122
  }
117
- buildModel() {
123
+ buildRawModel() {
118
124
  if ('last' in this.cachedModel) {
119
125
  logger.debug('[ModelBuilder] returning cached model');
120
126
  return this.cachedModel.last;
@@ -133,6 +139,43 @@ export class LikeC4ModelBuilder {
133
139
  return null;
134
140
  }
135
141
  }
142
+ buildModel() {
143
+ const model = this.buildRawModel();
144
+ if (!model) {
145
+ return null;
146
+ }
147
+ const index = ModelIndex.from(model);
148
+ const views = R.pipe(R.values(model.views), R.map(view => computeView(view, index).view), R.compact);
149
+ assignNavigateTo(views);
150
+ return {
151
+ elements: model.elements,
152
+ relations: model.relations,
153
+ views: R.mapToObj(views, v => [v.id, v])
154
+ };
155
+ }
156
+ computeView(viewId) {
157
+ const model = this.buildRawModel();
158
+ const view = model?.views[viewId];
159
+ if (!view) {
160
+ logger.warn(`[ModelBuilder] Cannot find view ${viewId}`);
161
+ return null;
162
+ }
163
+ const result = computeView(view, ModelIndex.from(model));
164
+ if (!result.isSuccess) {
165
+ logError(result.error);
166
+ return null;
167
+ }
168
+ const allElementViews = R.values(model.views).filter((v) => isStrictElementView(v) && v.id !== viewId);
169
+ const computedView = result.view;
170
+ computedView.nodes.forEach(node => {
171
+ // find first element view that is not the current one
172
+ const navigateTo = R.find(allElementViews, v => v.viewOf === node.id);
173
+ if (navigateTo) {
174
+ node.navigateTo = navigateTo.id;
175
+ }
176
+ });
177
+ return computedView;
178
+ }
136
179
  scheduledCb = null;
137
180
  notifyClient() {
138
181
  const connection = this.services.shared.lsp.Connection;
@@ -91,7 +91,7 @@ export class LikeC4ModelLocator {
91
91
  if (node.name) {
92
92
  targetNode = findNodeForProperty(node.$cstNode, 'name') ?? targetNode;
93
93
  }
94
- else if (node.viewOf) {
94
+ else if ('viewOf' in node) {
95
95
  targetNode = findNodeForProperty(node.$cstNode, 'viewOf') ?? targetNode;
96
96
  }
97
97
  if (!targetNode) {
@@ -4,7 +4,7 @@ import objectHash from 'object-hash';
4
4
  import stripIndent from 'strip-indent';
5
5
  import { Disposable } from 'vscode-languageserver-protocol';
6
6
  import { ElementViewOps, ast, cleanParsedModel, isLikeC4LangiumDocument, resolveRelationPoints, streamModel, toAutoLayout, toElementStyle, toElementStyleExcludeDefaults } from '../ast';
7
- import { elementRef, strictElementRefFqn } from '../elementRef';
7
+ import { elementRef, fqnElementRef } from '../elementRef';
8
8
  import { logError, logWarnError, logger } from '../logger';
9
9
  import { printDocs } from '../utils';
10
10
  export class LikeC4ModelParser {
@@ -226,31 +226,29 @@ export class LikeC4ModelParser {
226
226
  nonexhaustive(astRule);
227
227
  }
228
228
  parseElementView(astNode) {
229
- const viewOfEl = astNode.viewOf && elementRef(astNode.viewOf);
230
- const viewOf = viewOfEl && this.resolveFqn(viewOfEl);
229
+ const body = astNode.body;
230
+ invariant(body, 'ElementView body is not defined');
231
231
  const astPath = this.getAstNodePath(astNode);
232
232
  let id = astNode.name;
233
233
  if (!id) {
234
234
  const doc = getDocument(astNode).uri.toString();
235
235
  id = objectHash({
236
236
  doc,
237
- astPath,
238
- viewOf: viewOf ?? null
237
+ astPath
239
238
  });
240
239
  }
241
- const title = astNode.props.find(p => p.key === 'title')?.value;
242
- const description = astNode.props.find(p => p.key === 'description')?.value;
243
- const tags = this.convertTags(astNode);
244
- const links = astNode.props.filter(ast.isLinkProperty).map(p => p.value);
245
- return {
246
- id,
240
+ const title = body.props.find(p => p.key === 'title')?.value;
241
+ const description = body.props.find(p => p.key === 'description')?.value;
242
+ const tags = this.convertTags(body);
243
+ const links = body.props.filter(ast.isLinkProperty).map(p => p.value);
244
+ const basic = {
245
+ id: id,
247
246
  astPath,
248
- ...(viewOf && { viewOf }),
249
247
  ...(title && { title }),
250
248
  ...(description && { description }),
251
249
  ...(tags && { tags }),
252
- ...(links && isNonEmptyArray(links) && { links }),
253
- rules: astNode.rules.flatMap(n => {
250
+ ...(isNonEmptyArray(links) && { links }),
251
+ rules: body.rules.flatMap(n => {
254
252
  try {
255
253
  return this.parseViewRule(n);
256
254
  }
@@ -260,10 +258,28 @@ export class LikeC4ModelParser {
260
258
  }
261
259
  })
262
260
  };
261
+ if ('viewOf' in astNode) {
262
+ const viewOfEl = elementRef(astNode.viewOf);
263
+ const viewOf = viewOfEl && this.resolveFqn(viewOfEl);
264
+ invariant(viewOf, ' viewOf is not resolved: ' + astNode.$cstNode?.text);
265
+ return {
266
+ ...basic,
267
+ viewOf
268
+ };
269
+ }
270
+ if ('extends' in astNode) {
271
+ const extendsView = astNode.extends.view.ref;
272
+ invariant(extendsView?.name, 'view extends is not resolved: ' + astNode.$cstNode?.text);
273
+ return {
274
+ ...basic,
275
+ extends: extendsView.name
276
+ };
277
+ }
278
+ return basic;
263
279
  }
264
280
  resolveFqn(node) {
265
281
  if (ast.isExtendElement(node)) {
266
- return strictElementRefFqn(node.element);
282
+ return fqnElementRef(node.element);
267
283
  }
268
284
  const fqn = this.fqnIndex.getFqn(node);
269
285
  invariant(fqn, `Not indexed element: ${this.getAstNodePath(node)}`);
@@ -1,6 +1,6 @@
1
- import type { Fqn, LikeC4Model, RelationID, ViewID } from '@likec4/core';
1
+ import type { ComputedView, Fqn, LikeC4Model, LikeC4RawModel, RelationID, ViewID } from '@likec4/core';
2
2
  import type { DocumentUri, Location } from 'vscode-languageserver-protocol';
3
- import { NotificationType, RequestType0, RequestType } from 'vscode-languageserver-protocol';
3
+ import { NotificationType, RequestType, RequestType0 } from 'vscode-languageserver-protocol';
4
4
  interface BuildDocumentsParams {
5
5
  docs: DocumentUri[];
6
6
  }
@@ -18,6 +18,14 @@ export declare const Rpc: {
18
18
  readonly fetchModel: RequestType0<{
19
19
  model: LikeC4Model | null;
20
20
  }, void>;
21
+ readonly fetchRawModel: RequestType0<{
22
+ rawmodel: LikeC4RawModel | null;
23
+ }, void>;
24
+ readonly computeView: RequestType<{
25
+ viewId: ViewID;
26
+ }, {
27
+ view: ComputedView | null;
28
+ }, void>;
21
29
  readonly rebuild: RequestType0<{
22
30
  docs: DocumentUri[];
23
31
  }, void>;
package/dist/protocol.js CHANGED
@@ -1,9 +1,11 @@
1
- import { NotificationType, RequestType0, RequestType } from 'vscode-languageserver-protocol';
1
+ import { NotificationType, RequestType, RequestType0 } from 'vscode-languageserver-protocol';
2
2
  //#region From server
3
3
  const onDidChangeModel = new NotificationType('likec4/onDidChangeModel');
4
4
  //#endregion
5
5
  //#region To server
6
+ const fetchRawModel = new RequestType0('likec4/fetchRaw');
6
7
  const fetchModel = new RequestType0('likec4/fetchModel');
8
+ const computeView = new RequestType('likec4/computeView');
7
9
  const rebuild = new RequestType0('likec4/rebuildModel');
8
10
  const buildDocuments = new RequestType('likec4/buildDocuments');
9
11
  export const locate = new RequestType('likec4/locate');
@@ -11,6 +13,8 @@ export const locate = new RequestType('likec4/locate');
11
13
  export const Rpc = {
12
14
  onDidChangeModel,
13
15
  fetchModel,
16
+ fetchRawModel,
17
+ computeView,
14
18
  rebuild,
15
19
  buildDocuments,
16
20
  locate
@@ -1,6 +1,6 @@
1
1
  import { DONE_RESULT, DefaultScopeProvider, EMPTY_STREAM, StreamImpl, StreamScope, getDocument, stream, findNodeForProperty, toDocumentSegment } from 'langium';
2
2
  import { ast } from '../ast';
3
- import { elementRef, isElementRefHead, parentStrictElementRef } from '../elementRef';
3
+ import { elementRef, isElementRefHead, parentFqnElementRef } from '../elementRef';
4
4
  import { logError } from '../logger';
5
5
  function toAstNodeDescription(entry) {
6
6
  const $cstNode = findNodeForProperty(entry.el.$cstNode, 'name');
@@ -52,11 +52,17 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
52
52
  scopeExtendElement(extend) {
53
53
  return this.uniqueDescedants(() => elementRef(extend.element));
54
54
  }
55
- scopeElementView({ viewOf }) {
56
- if (!viewOf) {
57
- return EMPTY_STREAM;
55
+ scopeElementView({ viewOf, extends: ext }) {
56
+ if (ext) {
57
+ return stream([ext]).flatMap(v => {
58
+ const view = v.view.ref;
59
+ return view ? this.scopeElementView(view) : EMPTY_STREAM;
60
+ });
58
61
  }
59
- return this.uniqueDescedants(() => elementRef(viewOf));
62
+ if (viewOf) {
63
+ return this.uniqueDescedants(() => elementRef(viewOf));
64
+ }
65
+ return EMPTY_STREAM;
60
66
  }
61
67
  getScope(context) {
62
68
  const referenceType = this.reflection.getReferenceType(context);
@@ -68,7 +74,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
68
74
  if (isElementRefHead(container)) {
69
75
  return this.getGlobalScope(referenceType);
70
76
  }
71
- const parent = parentStrictElementRef(container);
77
+ const parent = parentFqnElementRef(container);
72
78
  return new StreamScope(this.directChildrenOf(parent));
73
79
  }
74
80
  if (ast.isElementRef(container) && !isElementRefHead(container)) {
@@ -102,7 +108,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
102
108
  if (ast.isExtendElementBody(container)) {
103
109
  scopes.push(this.scopeExtendElement(container.$container));
104
110
  }
105
- if (ast.isViewRule(container)) {
111
+ if (ast.isElementViewBody(container)) {
106
112
  scopes.push(this.scopeElementView(container.$container));
107
113
  }
108
114
  }
@@ -22,6 +22,22 @@ export function registerProtocolHandlers(services) {
22
22
  }
23
23
  return Promise.resolve({ model });
24
24
  });
25
+ connection.onRequest(Rpc.fetchRawModel, async (_cancelToken) => {
26
+ let rawmodel;
27
+ try {
28
+ rawmodel = modelBuilder.buildRawModel() ?? null;
29
+ }
30
+ catch (e) {
31
+ rawmodel = null;
32
+ logError(e);
33
+ }
34
+ return Promise.resolve({ rawmodel });
35
+ });
36
+ connection.onRequest(Rpc.computeView, ({ viewId }) => {
37
+ return {
38
+ view: modelBuilder.computeView(viewId)
39
+ };
40
+ });
25
41
  connection.onRequest(Rpc.rebuild, async (cancelToken) => {
26
42
  const changed = LangiumDocuments.all
27
43
  .map(d => {
@@ -2,6 +2,9 @@ import { ast } from '../ast';
2
2
  export const viewChecks = (services) => {
3
3
  const index = services.shared.workspace.IndexManager;
4
4
  return (el, accept) => {
5
+ if (el.extends) {
6
+ // TODO: circular dependency check
7
+ }
5
8
  if (!el.name) {
6
9
  return;
7
10
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@likec4/language-server",
3
3
  "description": "LikeC4 Language Server",
4
- "version": "0.36.0",
4
+ "version": "0.37.1",
5
5
  "license": "MIT",
6
6
  "bugs": "https://github.com/likec4/likec4/issues",
7
7
  "homepage": "https://likec4.dev",
@@ -42,7 +42,7 @@
42
42
  "test:watch": "vitest"
43
43
  },
44
44
  "dependencies": {
45
- "@likec4/core": "0.36.0",
45
+ "@likec4/core": "0.37.1",
46
46
  "langium": "^2.0.2",
47
47
  "nanoid": "^4.0.2",
48
48
  "object-hash": "^3.0.0",
@@ -59,7 +59,7 @@
59
59
  "langium-cli": "^2.0.1",
60
60
  "npm-run-all": "^4.1.5",
61
61
  "typescript": "^5.2.2",
62
- "vitest": "^0.34.3"
62
+ "vitest": "^0.34.4"
63
63
  },
64
64
  "packageManager": "yarn@3.6.3"
65
65
  }