@likec4/language-server 0.25.0 → 0.27.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.
@@ -3,11 +3,11 @@
3
3
  * This program and the accompanying materials are made available under the
4
4
  * terms of the MIT License, which is available in the project root.
5
5
  ******************************************************************************/
6
- import invariant from 'tiny-invariant';
7
6
  import { findNodeForProperty } from 'langium';
8
7
  import { SymbolKind } from 'vscode-languageserver-protocol';
9
8
  import { ast } from '../ast';
10
9
  import { logger } from '../logger';
10
+ import { isEmpty } from 'remeda';
11
11
  export class LikeC4DocumentSymbolProvider {
12
12
  services;
13
13
  constructor(services) {
@@ -28,10 +28,20 @@ export class LikeC4DocumentSymbolProvider {
28
28
  const specKeywordNode = findNodeForProperty(cstModel, 'name');
29
29
  if (!specKeywordNode)
30
30
  return [];
31
- const specSymbols = [];
31
+ const specSymbols = astSpec.specs.flatMap(nd => {
32
+ if (ast.isSpecificationElementKind(nd)) {
33
+ return getElementKindSymbol(nd) ?? [];
34
+ }
35
+ else if (ast.isSpecificationTag(nd)) {
36
+ return getTagSymbol(nd) ?? [];
37
+ }
38
+ else {
39
+ return [];
40
+ }
41
+ });
32
42
  const getElementKindSymbol = (astKind) => {
33
- invariant(astKind.$cstNode, 'SpecificationElementKind must have a CST node');
34
- invariant(astKind.kind.$cstNode, 'SpecificationElementKind name must have a CST node');
43
+ if (!astKind.$cstNode || !astKind.kind.$cstNode || isEmpty(astKind.kind.name))
44
+ return null;
35
45
  return {
36
46
  kind: SymbolKind.Class,
37
47
  name: astKind.kind.name,
@@ -39,17 +49,9 @@ export class LikeC4DocumentSymbolProvider {
39
49
  selectionRange: astKind.kind.$cstNode.range
40
50
  };
41
51
  };
42
- for (const astKind of astSpec.elementKinds) {
43
- try {
44
- specSymbols.push(getElementKindSymbol(astKind));
45
- }
46
- catch (e) {
47
- logger.error(e);
48
- }
49
- }
50
52
  const getTagSymbol = (astTag) => {
51
- invariant(astTag.$cstNode, 'TagSpec must have a CST node');
52
- invariant(astTag.tag.$cstNode, 'Tag name must have a CST node');
53
+ if (!astTag.$cstNode || !astTag.tag.$cstNode || isEmpty(astTag.tag.name))
54
+ return null;
53
55
  return {
54
56
  kind: SymbolKind.EnumMember,
55
57
  name: '#' + astTag.tag.name,
@@ -57,14 +59,6 @@ export class LikeC4DocumentSymbolProvider {
57
59
  selectionRange: astTag.tag.$cstNode.range
58
60
  };
59
61
  };
60
- for (const astTag of astSpec.tags) {
61
- try {
62
- specSymbols.push(getTagSymbol(astTag));
63
- }
64
- catch (e) {
65
- logger.error(e);
66
- }
67
- }
68
62
  if (specSymbols.length === 0)
69
63
  return [];
70
64
  return [
@@ -1,7 +1,7 @@
1
1
  import { failExpectedNever } from '@likec4/core';
2
2
  import { Fqn } from '@likec4/core/types';
3
3
  import { MultiMap } from 'langium';
4
- import { isEmpty, isNil } from 'rambdax';
4
+ import { isEmpty, isNil } from 'remeda';
5
5
  import { ElementOps, ast } from '../ast';
6
6
  import { strictElementRefFqn } from '../elementRef';
7
7
  export function computeDocumentFqn(document, services) {
@@ -1,5 +1,5 @@
1
1
  import type { Fqn } from '@likec4/core/types';
2
- import type { LangiumDocuments } from 'langium';
2
+ import type { LangiumDocument, LangiumDocuments } from 'langium';
3
3
  import { StreamImpl } from 'langium';
4
4
  import type { ast } from '../ast';
5
5
  import { type LikeC4LangiumDocument } from '../ast';
@@ -7,6 +7,7 @@ import type { LikeC4Services } from '../module';
7
7
  type FqnIndexedDocument = Omit<LikeC4LangiumDocument, 'c4fqns'> & {
8
8
  c4fqns: NonNullable<LikeC4LangiumDocument['c4fqns']>;
9
9
  };
10
+ export declare function isFqnIndexedDocument(doc: LangiumDocument): doc is FqnIndexedDocument;
10
11
  export interface FqnIndexEntry {
11
12
  fqn: Fqn;
12
13
  name: string;
@@ -24,7 +25,7 @@ export declare class FqnIndex {
24
25
  path: string;
25
26
  doc: FqnIndexedDocument;
26
27
  }>;
27
- directChildrenOf(parent: Fqn): import("langium").Stream<FqnIndexEntry>;
28
+ directChildrenOf(parent: Fqn): StreamImpl<IterableIterator<FqnIndexEntry> | null, FqnIndexEntry>;
28
29
  uniqueDescedants(parent: Fqn): StreamImpl<IterableIterator<FqnIndexEntry> | null, FqnIndexEntry>;
29
30
  }
30
31
  export {};
@@ -1,10 +1,12 @@
1
1
  import { nameFromFqn, parentFqn } from '@likec4/core/utils';
2
- import { DocumentState, DONE_RESULT, getDocument, MultiMap, StreamImpl } from 'langium';
2
+ import { DONE_RESULT, DocumentState, MultiMap, StreamImpl } from 'langium';
3
3
  import { isNil } from 'remeda';
4
4
  import { ElementOps, isLikeC4LangiumDocument } from '../ast';
5
5
  import { logger } from '../logger';
6
6
  import { computeDocumentFqn } from './fqn-computation';
7
- const isFqnIndexedDocument = (doc) => isLikeC4LangiumDocument(doc) && doc.state >= DocumentState.IndexedContent && !isNil(doc.c4fqns);
7
+ export function isFqnIndexedDocument(doc) {
8
+ return isLikeC4LangiumDocument(doc) && doc.state >= DocumentState.IndexedContent && !isNil(doc.c4fqns);
9
+ }
8
10
  export class FqnIndex {
9
11
  services;
10
12
  langiumDocuments;
@@ -31,54 +33,70 @@ export class FqnIndex {
31
33
  return this.documents().flatMap(doc => doc.c4fqns.entries().map(([fqn, path]) => ({ fqn, path, doc })));
32
34
  }
33
35
  get(el) {
34
- let fqn = ElementOps.readId(el) ?? null;
35
- if (fqn) {
36
- const doc = getDocument(el);
37
- if (isFqnIndexedDocument(doc) && doc.c4fqns.has(fqn)) {
38
- return fqn;
39
- }
40
- const path = this.services.workspace.AstNodeLocator.getAstNodePath(el);
41
- logger.error(`Clean cached FQN ${fqn} at ${path}`);
42
- ElementOps.writeId(el, null);
43
- fqn = null;
44
- }
45
- return fqn;
36
+ return ElementOps.readId(el) ?? null;
37
+ // if (fqn) {
38
+ // const doc = getDocument(el)
39
+ // if (isFqnIndexedDocument(doc) && doc.c4fqns.has(fqn)) {
40
+ // return fqn
41
+ // }
42
+ // const path = this.services.workspace.AstNodeLocator.getAstNodePath(el)
43
+ // logger.error(`Clean cached FQN ${fqn} at ${path}`)
44
+ // ElementOps.writeId(el, null)
45
+ // fqn = null
46
+ // }
47
+ // return fqn
46
48
  }
47
49
  byFqn(fqn) {
48
- return this.documents()
49
- .flatMap(doc => {
50
+ return this.documents().flatMap(doc => {
50
51
  return doc.c4fqns.get(fqn).map(path => ({ path, doc }));
51
52
  });
52
53
  }
53
54
  directChildrenOf(parent) {
54
- return this
55
- .entries()
56
- .filter(e => parentFqn(e.fqn) === parent)
57
- .map((e) => ({ ...e, name: nameFromFqn(e.fqn) }));
55
+ return new StreamImpl(() => {
56
+ const children = new MultiMap(this.entries()
57
+ .filter(e => parentFqn(e.fqn) === parent)
58
+ .map((e) => {
59
+ const name = nameFromFqn(e.fqn);
60
+ const entry = { ...e, name };
61
+ return [name, entry];
62
+ })
63
+ .toArray());
64
+ if (children.size === 0) {
65
+ return null;
66
+ }
67
+ return children
68
+ .entriesGroupedByKey()
69
+ .flatMap(([_name, descrs]) => (descrs.length === 1 ? descrs : []))
70
+ .iterator();
71
+ }, iterator => {
72
+ if (iterator) {
73
+ return iterator.next();
74
+ }
75
+ return DONE_RESULT;
76
+ });
58
77
  }
59
78
  uniqueDescedants(parent) {
60
79
  return new StreamImpl(() => {
61
80
  const prefix = `${parent}.`;
62
- const children = [];
63
81
  const childrenNames = new Set();
64
82
  const descedants = [];
65
- this.entries().forEach(e => {
66
- if (e.fqn.startsWith(prefix)) {
67
- const name = nameFromFqn(e.fqn);
68
- const entry = { ...e, name };
69
- if (parentFqn(e.fqn) === parent) {
70
- childrenNames.add(name);
71
- children.push(entry);
72
- }
73
- else {
74
- descedants.push(entry);
75
- }
83
+ const nested = new MultiMap();
84
+ this.entries()
85
+ .filter(e => e.fqn.startsWith(prefix))
86
+ .forEach(e => {
87
+ const name = nameFromFqn(e.fqn);
88
+ const entry = { ...e, name };
89
+ if (parentFqn(e.fqn) === parent) {
90
+ childrenNames.add(name);
91
+ nested.add(name, entry);
92
+ }
93
+ else {
94
+ descedants.push(entry);
76
95
  }
77
96
  });
78
- if (children.length + descedants.length === 0) {
97
+ if (nested.size + descedants.length === 0) {
79
98
  return null;
80
99
  }
81
- const nested = new MultiMap(children.map(entry => [entry.name, entry]));
82
100
  for (const descedant of descedants) {
83
101
  if (!childrenNames.has(descedant.name)) {
84
102
  nested.add(descedant.name, descedant);
@@ -1,17 +1,17 @@
1
- import { computeViews } from '@likec4/core';
1
+ import { ModelIndex, assignNavigateTo, computeView } from '@likec4/core';
2
2
  import { DefaultElementShape, DefaultThemeColor } from '@likec4/core/types';
3
3
  import { compareByFqnHierarchically, parentFqn } from '@likec4/core/utils';
4
4
  import { DocumentState, getDocument } from 'langium';
5
5
  import objectHash from 'object-hash';
6
- import { clone, isNil } from 'rambdax';
6
+ import { clone } from 'rambdax';
7
7
  import * as R from 'remeda';
8
+ import stripIndent from 'strip-indent';
8
9
  import invariant from 'tiny-invariant';
9
10
  import { ElementViewOps, ast, cleanParsedModel, isLikeC4LangiumDocument, isValidLikeC4LangiumDocument, resolveRelationPoints, streamModel, toAutoLayout, toElementStyle } from '../ast';
10
11
  import { elementRef, strictElementRefFqn } from '../elementRef';
11
12
  import { logger } from '../logger';
12
13
  import { Rpc } from '../protocol';
13
14
  import { failExpectedNever } from '../utils';
14
- import stripIndent from 'strip-indent';
15
15
  export class LikeC4ModelBuilder {
16
16
  services;
17
17
  fqnIndex;
@@ -47,7 +47,7 @@ export class LikeC4ModelBuilder {
47
47
  return this.services.shared.lsp.Connection;
48
48
  }
49
49
  documents() {
50
- return this.langiumDocuments.all.toArray().filter(isValidLikeC4LangiumDocument);
50
+ return this.langiumDocuments.all.filter(isValidLikeC4LangiumDocument).toArray();
51
51
  }
52
52
  buildModel() {
53
53
  const docs = this.documents();
@@ -73,10 +73,6 @@ export class LikeC4ModelBuilder {
73
73
  }
74
74
  return null;
75
75
  };
76
- const toModelRelation = (rel) => {
77
- const { astPath, ...model } = rel;
78
- return model;
79
- };
80
76
  const elements = R.pipe(R.flatMap(docs, d => d.c4Elements), R.map(toModelElement), R.compact, R.sort(compareByFqnHierarchically), R.reduce((acc, el) => {
81
77
  const parent = parentFqn(el.id);
82
78
  if (!parent || parent in acc) {
@@ -88,7 +84,18 @@ export class LikeC4ModelBuilder {
88
84
  }
89
85
  return acc;
90
86
  }, {}));
91
- const relations = R.pipe(R.flatMap(docs, d => d.c4Relations), R.map(toModelRelation), R.filter(({ source, target }) => source in elements && target in elements), R.mapToObj(r => [r.id, r]));
87
+ const toModelRelation = ({ astPath, source, target, ...model }) => {
88
+ if (source in elements && target in elements) {
89
+ return {
90
+ source,
91
+ target,
92
+ ...model
93
+ };
94
+ }
95
+ return null;
96
+ };
97
+ const relations = R.pipe(R.flatMap(docs, d => d.c4Relations), R.map(toModelRelation), R.compact, R.mapToObj(r => [r.id, r]));
98
+ const modelIndex = ModelIndex.from({ elements, relations });
92
99
  const toModelView = (view) => {
93
100
  // eslint-disable-next-line prefer-const
94
101
  let { astPath, rules, title, ...model } = view;
@@ -98,18 +105,19 @@ export class LikeC4ModelBuilder {
98
105
  if (!title && view.id === 'index') {
99
106
  title = 'Landscape view';
100
107
  }
101
- return {
108
+ return computeView({
102
109
  ...model,
103
110
  ...(title && { title }),
104
111
  rules: clone(rules)
105
- };
112
+ }, modelIndex);
106
113
  };
107
- const views = R.pipe(docs, R.flatMap(d => d.c4Views), R.map(toModelView), R.filter(v => isNil(v.viewOf) || v.viewOf in elements), R.mapToObj(v => [v.id, v]));
108
- return computeViews({
114
+ const views = R.pipe(R.flatMap(docs, d => d.c4Views), R.map(toModelView), R.compact);
115
+ assignNavigateTo(views);
116
+ return {
109
117
  elements,
110
118
  relations,
111
- views
112
- });
119
+ views: R.mapToObj(views, v => [v.id, v])
120
+ };
113
121
  }
114
122
  catch (e) {
115
123
  logger.error(e);
@@ -121,9 +129,9 @@ export class LikeC4ModelBuilder {
121
129
  */
122
130
  parseDocument(doc) {
123
131
  const { elements, relations, views, specification } = cleanParsedModel(doc);
124
- const spec = doc.parseResult.value.specification;
125
- if (spec) {
126
- for (const { kind, style } of spec.elementKinds) {
132
+ const specs = doc.parseResult.value.specification?.specs.filter(ast.isSpecificationElementKind);
133
+ if (specs) {
134
+ for (const { kind, style } of specs) {
127
135
  try {
128
136
  const styleProps = toElementStyle(style?.props);
129
137
  specification.kinds[kind.name] = {
@@ -1,5 +1,5 @@
1
- import { findNodeForKeyword, findNodeForProperty, getDocument } from 'langium';
2
- import { ast, isParsedLikeC4LangiumDocument } from '../ast';
1
+ import { findNodeForProperty, getDocument } from 'langium';
2
+ import { ElementOps, ast, isParsedLikeC4LangiumDocument } from '../ast';
3
3
  export class LikeC4ModelLocator {
4
4
  services;
5
5
  fqnIndex;
@@ -10,21 +10,21 @@ export class LikeC4ModelLocator {
10
10
  this.langiumDocuments = services.shared.workspace.LangiumDocuments;
11
11
  }
12
12
  documents() {
13
- return this.langiumDocuments.all.toArray().filter(isParsedLikeC4LangiumDocument);
13
+ return this.langiumDocuments.all.filter(isParsedLikeC4LangiumDocument);
14
14
  }
15
15
  getParsedElement(astNode) {
16
+ const fqn = ElementOps.readId(astNode) ?? null;
17
+ if (!fqn)
18
+ return null;
16
19
  const doc = getDocument(astNode);
17
20
  if (!isParsedLikeC4LangiumDocument(doc)) {
18
21
  return null;
19
22
  }
20
- const fqn = this.fqnIndex.get(astNode);
21
- if (!fqn)
22
- return null;
23
23
  return doc.c4Elements.find(e => e.id === fqn) ?? null;
24
24
  }
25
25
  locateElement(fqn, property = 'name') {
26
26
  for (const doc of this.documents()) {
27
- if (doc.c4fqns && !doc.c4fqns.has(fqn)) {
27
+ if (!doc.c4fqns?.has(fqn)) {
28
28
  continue;
29
29
  }
30
30
  const element = doc.c4Elements.find(e => e.id === fqn);
@@ -68,7 +68,7 @@ export class LikeC4ModelLocator {
68
68
  };
69
69
  }
70
70
  }
71
- const targetNode = findNodeForKeyword(node.$cstNode, '->');
71
+ const targetNode = findNodeForProperty(node.$cstNode, 'arr');
72
72
  if (!targetNode) {
73
73
  return null;
74
74
  }
package/dist/module.js CHANGED
@@ -4,7 +4,7 @@ import { LikeC4DocumentSymbolProvider, LikeC4HoverProvider, LikeC4SemanticTokenP
4
4
  import { FqnIndex, LikeC4ModelBuilder, LikeC4ModelLocator } from './model';
5
5
  import { LikeC4ScopeComputation, LikeC4ScopeProvider } from './references';
6
6
  import { registerProtocolHandlers } from './registerProtocolHandlers';
7
- import { LikeC4CodeLensProvider, LikeC4WorkspaceManager } from './shared';
7
+ import { LikeC4CodeLensProvider, LikeC4DocumentLinkProvider, LikeC4WorkspaceManager } from './shared';
8
8
  import { registerValidationChecks } from './validation';
9
9
  import { logger } from './logger';
10
10
  function bind(Type) {
@@ -36,7 +36,8 @@ const LikeC4SharedModule = {
36
36
  WorkspaceManager: services => new LikeC4WorkspaceManager(services)
37
37
  },
38
38
  lsp: {
39
- CodeLensProvider: services => new LikeC4CodeLensProvider(services)
39
+ CodeLensProvider: services => new LikeC4CodeLensProvider(services),
40
+ DocumentLinkProvider: services => new LikeC4DocumentLinkProvider(services)
40
41
  }
41
42
  };
42
43
  export function createLanguageServices(context) {
@@ -1,5 +1,6 @@
1
1
  import { DefaultScopeComputation, MultiMap } from 'langium';
2
2
  import { ast } from '../ast';
3
+ import { isEmpty } from 'remeda';
3
4
  export class LikeC4ScopeComputation extends DefaultScopeComputation {
4
5
  services;
5
6
  constructor(services) {
@@ -9,28 +10,30 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
9
10
  computeExports(document, _cancelToken) {
10
11
  const { specification, model, views } = document.parseResult.value;
11
12
  const docExports = [];
12
- if (specification) {
13
- for (const { kind } of specification.elementKinds) {
14
- docExports.push(this.descriptions.createDescription(kind, kind.name, document));
15
- }
16
- for (const { tag } of specification.tags) {
17
- docExports.push(this.descriptions.createDescription(tag, tag.name, document));
18
- docExports.push(this.descriptions.createDescription(tag, '#' + tag.name, document));
13
+ if (specification && specification.specs.length > 0) {
14
+ for (const spec of specification.specs) {
15
+ if (ast.isSpecificationElementKind(spec) && spec.kind && !isEmpty(spec.kind.name)) {
16
+ docExports.push(this.descriptions.createDescription(spec.kind, spec.kind.name, document));
17
+ continue;
18
+ }
19
+ if (ast.isSpecificationTag(spec) && spec.tag && !isEmpty(spec.tag.name)) {
20
+ docExports.push(this.descriptions.createDescription(spec.tag, spec.tag.name, document));
21
+ docExports.push(this.descriptions.createDescription(spec.tag, '#' + spec.tag.name, document));
22
+ continue;
23
+ }
19
24
  }
20
25
  }
21
- // Filled later by FqnIndex (on IndexedContent phase)
22
- document.c4fqns = undefined;
23
26
  // Only root model elements are exported
24
27
  if (model && model.elements.length > 0) {
25
28
  for (const elAst of model.elements) {
26
- if (ast.isElement(elAst)) {
29
+ if (ast.isElement(elAst) && !isEmpty(elAst.name)) {
27
30
  docExports.push(this.descriptions.createDescription(elAst, elAst.name, document));
28
31
  }
29
32
  }
30
33
  }
31
34
  if (views && views.views.length > 0) {
32
35
  for (const viewAst of views.views) {
33
- if ('name' in viewAst) {
36
+ if (viewAst.name && !isEmpty(viewAst.name)) {
34
37
  docExports.push(this.descriptions.createDescription(viewAst, viewAst.name, document));
35
38
  }
36
39
  }
@@ -54,7 +57,7 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
54
57
  continue;
55
58
  }
56
59
  let subcontainer;
57
- if (ast.isElement(el)) {
60
+ if (ast.isElement(el) && !isEmpty(el.name)) {
58
61
  localScope.add(el.name, this.descriptions.createDescription(el, el.name, document));
59
62
  subcontainer = el.body;
60
63
  }
@@ -0,0 +1,8 @@
1
+ import type { LangiumDocument, LangiumSharedServices, MaybePromise, DocumentLinkProvider } from 'langium';
2
+ import type { DocumentLink, DocumentLinkParams } from 'vscode-languageserver-protocol';
3
+ export declare class LikeC4DocumentLinkProvider implements DocumentLinkProvider {
4
+ private services;
5
+ constructor(services: LangiumSharedServices);
6
+ getDocumentLinks(doc: LangiumDocument, _params: DocumentLinkParams): MaybePromise<DocumentLink[]>;
7
+ }
8
+ //# sourceMappingURL=DocumentLinkProvider.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { findNodeForProperty, streamAllContents } from 'langium/lib/utils';
2
+ import { ast, isParsedLikeC4LangiumDocument } from '../ast';
3
+ import { logger } from '../logger';
4
+ export class LikeC4DocumentLinkProvider {
5
+ services;
6
+ constructor(services) {
7
+ this.services = services;
8
+ //
9
+ }
10
+ getDocumentLinks(doc, _params) {
11
+ if (!isParsedLikeC4LangiumDocument(doc)) {
12
+ return [];
13
+ }
14
+ const base = new URL(doc.uri.toString());
15
+ return streamAllContents(doc.parseResult.value)
16
+ .filter(ast.isLinkProperty)
17
+ .flatMap((n) => {
18
+ try {
19
+ const u = new URL(n.value, base);
20
+ const valueCst = findNodeForProperty(n.$cstNode, 'value');
21
+ if (!valueCst) {
22
+ return [];
23
+ }
24
+ return {
25
+ range: valueCst.range,
26
+ target: u.toString()
27
+ };
28
+ }
29
+ catch (e) {
30
+ logger.error(e);
31
+ return [];
32
+ }
33
+ })
34
+ .toArray();
35
+ }
36
+ }
37
+ //# sourceMappingURL=DocumentLinkProvider.js.map
@@ -1,3 +1,4 @@
1
1
  export * from './CodeLensProvider';
2
2
  export * from './WorkspaceManager';
3
+ export * from './DocumentLinkProvider';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export * from './CodeLensProvider';
2
2
  export * from './WorkspaceManager';
3
+ export * from './DocumentLinkProvider';
3
4
  //# sourceMappingURL=index.js.map