@likec4/language-server 1.46.2 → 1.46.4

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.
@@ -15,85 +15,86 @@ export class LikeC4ModelChanges {
15
15
  }
16
16
  async applyChange(changeView) {
17
17
  const lspConnection = this.services.shared.lsp.Connection;
18
- let result = null;
19
18
  try {
20
- await this.services.shared.workspace.WorkspaceLock.write(async () => {
21
- let { viewId, projectId: _projectId, change } = changeView;
22
- const project = this.services.shared.workspace.ProjectsManager.ensureProject(_projectId);
23
- logger.debug `Applying model change ${change.op} to view ${viewId} in project ${project.id}`;
24
- const lookup = this.locator.locateViewAst(viewId, project.id);
25
- if (!lookup) {
26
- throw new Error(`View ${viewId} not found in project ${project.id}`);
19
+ let { viewId, projectId: _projectId, change } = changeView;
20
+ const project = this.services.shared.workspace.ProjectsManager.ensureProject(_projectId);
21
+ logger.debug `Applying model change ${change.op} to view ${viewId} in project ${project.id}`;
22
+ const lookup = this.locator.locateViewAst(viewId, project.id);
23
+ if (!lookup) {
24
+ throw new Error(`View ${viewId} not found in project ${project.id}`);
25
+ }
26
+ const textDocument = {
27
+ uri: lookup.doc.textDocument.uri,
28
+ version: lookup.doc.textDocument.version,
29
+ };
30
+ // TODO refactor to use separate methods for save/reset operations
31
+ if (change.op === 'save-view-snapshot') {
32
+ invariant(viewId === change.layout.id, 'View ID does not match, expected ' + viewId + ', got ' + change.layout.id);
33
+ // If there is an existing manual layout v1
34
+ if (lookup.view.manualLayout) {
35
+ // We clean it up
36
+ await removeManualLayoutV1(this.services, { lookup }).catch(err => {
37
+ logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
38
+ });
27
39
  }
28
- const textDocument = {
29
- uri: lookup.doc.textDocument.uri,
30
- version: lookup.doc.textDocument.version,
40
+ const location = await this.services.likec4.ManualLayouts.write(project, change.layout);
41
+ return {
42
+ success: true,
43
+ location,
31
44
  };
32
- // TODO refactor to use separate methods for save/reset operations
33
- if (change.op === 'save-view-snapshot') {
34
- invariant(viewId === change.layout.id, 'View ID does not match, expected ' + viewId + ', got ' + change.layout.id);
35
- // If there is an existing manual layout v1
36
- if (lookup.view.manualLayout) {
37
- // We clean it up
38
- await removeManualLayoutV1(this.services, { lookup }).catch(err => {
39
- logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
40
- });
41
- }
42
- const location = await this.services.likec4.ManualLayouts.write(project, change.layout);
43
- result = {
44
- success: true,
45
- location,
46
- };
47
- return;
48
- }
49
- if (change.op === 'reset-manual-layout') {
50
- // If there is an existing manual layout v1
51
- if (lookup.view.manualLayout) {
52
- // We clean it up
53
- await removeManualLayoutV1(this.services, { lookup }).catch(err => {
54
- logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
55
- });
56
- }
57
- const location = await this.services.likec4.ManualLayouts.remove(project, viewId);
58
- result = {
59
- success: true,
60
- location,
61
- };
62
- return;
63
- }
64
- invariant(lspConnection, 'This change only supported in IDE (running as Extension)');
65
- const { edits, modifiedRange } = this.convertToTextEdit({
66
- lookup,
67
- change,
68
- });
69
- if (!edits.length) {
70
- return;
71
- }
72
- const applyResult = await lspConnection.workspace.applyEdit({
73
- label: `LikeC4 - change view ${changeView.viewId}`,
74
- edit: {
75
- changes: {
76
- [textDocument.uri]: edits,
77
- },
78
- },
79
- });
80
- if (!applyResult.applied) {
81
- lspConnection.window.showErrorMessage(`Failed to apply changes ${applyResult.failureReason}`);
82
- return;
45
+ }
46
+ if (change.op === 'reset-manual-layout') {
47
+ // If there is an existing manual layout v1
48
+ if (lookup.view.manualLayout) {
49
+ // We clean it up
50
+ await removeManualLayoutV1(this.services, { lookup }).catch(err => {
51
+ logger.warn(`Failed to remove manual layout v1 for view ${viewId} in project ${project.id}`, { err });
52
+ });
83
53
  }
84
- result = {
54
+ const location = await this.services.likec4.ManualLayouts.remove(project, viewId);
55
+ return {
85
56
  success: true,
86
- location: {
87
- uri: textDocument.uri,
88
- range: modifiedRange,
89
- },
57
+ location,
90
58
  };
59
+ }
60
+ invariant(lspConnection, 'This change only supported in IDE (running as Extension)');
61
+ const { edits, modifiedRange } = this.convertToTextEdit({
62
+ lookup,
63
+ change,
91
64
  });
65
+ if (!edits.length) {
66
+ return {
67
+ success: false,
68
+ error: 'No changes to apply',
69
+ };
70
+ }
71
+ const applyResult = await lspConnection.workspace.applyEdit({
72
+ label: `LikeC4 - change view ${changeView.viewId}`,
73
+ edit: {
74
+ changes: {
75
+ [textDocument.uri]: edits,
76
+ },
77
+ },
78
+ });
79
+ if (!applyResult.applied) {
80
+ lspConnection.window.showErrorMessage(`Failed to apply changes ${applyResult.failureReason}`);
81
+ return {
82
+ success: false,
83
+ error: `Failed to apply changes ${applyResult.failureReason}`,
84
+ };
85
+ }
86
+ return {
87
+ success: true,
88
+ location: {
89
+ uri: textDocument.uri,
90
+ range: modifiedRange,
91
+ },
92
+ };
92
93
  }
93
94
  catch (err) {
94
95
  const error = loggable(wrapError(err, `Failed to apply change ${changeView.change.op} ${changeView.viewId}`));
95
96
  logger.error(error);
96
- result = {
97
+ return {
97
98
  success: false,
98
99
  error,
99
100
  };
@@ -101,10 +102,6 @@ export class LikeC4ModelChanges {
101
102
  finally {
102
103
  this.services.likec4.ModelBuilder.clearCache();
103
104
  }
104
- return result ?? {
105
- success: false,
106
- error: 'Unknown error applying model change',
107
- };
108
105
  }
109
106
  convertToTextEdit({ lookup, change }) {
110
107
  switch (change.op) {
@@ -10,7 +10,7 @@ export declare class LikeC4ScopeProvider extends DefaultScopeProvider {
10
10
  protected readonly indexManager: IndexManager;
11
11
  constructor(services: LikeC4Services);
12
12
  getScope(context: ReferenceInfo): Scope;
13
- protected genUniqueDescedants(of: () => ast.Element | ast.DeploymentNode | undefined): Generator<import("../ast").AstNodeDescriptionWithFqn, void, any>;
13
+ protected genUniqueDescedants(element: ast.Element | ast.DeploymentNode | undefined): Generator<import("../ast").AstNodeDescriptionWithFqn, void, any>;
14
14
  protected genScopeExtendElement({ element }: ast.ExtendElement): Generator<AstNodeDescription>;
15
15
  protected genScopeElementView({ viewOf, extends: ext }: ast.ElementView): Generator<AstNodeDescription>;
16
16
  protected getScopeForStrictFqnRef(projectId: ProjectId, container: ast.StrictFqnRef, context: ReferenceInfo): Scope;
@@ -54,8 +54,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
54
54
  }
55
55
  }
56
56
  // we need lazy resolving here
57
- *genUniqueDescedants(of) {
58
- const element = of();
57
+ *genUniqueDescedants(element) {
59
58
  if (!element) {
60
59
  return;
61
60
  }
@@ -83,7 +82,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
83
82
  };
84
83
  }
85
84
  // we make extended element resolvable inside ExtendElementBody
86
- yield* this.genUniqueDescedants(() => elementRef(element));
85
+ yield* this.genUniqueDescedants(elementRef(element));
87
86
  }
88
87
  *genScopeElementView({ viewOf, extends: ext }) {
89
88
  if (viewOf) {
@@ -92,7 +91,7 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
92
91
  if (viewOf.modelElement.value.$nodeDescription) {
93
92
  yield viewOf.modelElement.value.$nodeDescription;
94
93
  }
95
- yield* this.genUniqueDescedants(() => elementRef(viewOf));
94
+ yield* this.genUniqueDescedants(elementRef(viewOf));
96
95
  return;
97
96
  }
98
97
  if (ext) {
@@ -115,10 +114,10 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
115
114
  if (deploymentNode.value.$nodeDescription) {
116
115
  yield deploymentNode.value.$nodeDescription;
117
116
  }
118
- yield* this.genUniqueDescedants(() => {
119
- const target = deploymentNode.value.ref;
120
- return target && ast.isDeploymentNode(target) ? target : undefined;
121
- });
117
+ const target = deploymentNode.value.ref;
118
+ if (target && ast.isDeploymentNode(target)) {
119
+ yield* this.genUniqueDescedants(target);
120
+ }
122
121
  }
123
122
  streamForFqnRef(projectId, container, context) {
124
123
  const parent = container.parent;
@@ -130,25 +129,23 @@ export class LikeC4ScopeProvider extends DefaultScopeProvider {
130
129
  return EMPTY_STREAM;
131
130
  }
132
131
  if (ast.isImported(parentRef)) {
133
- return stream(this.genUniqueDescedants(() => {
134
- return parentRef.imported.ref;
135
- }));
132
+ return stream(this.genUniqueDescedants(parentRef.imported.ref));
136
133
  }
137
134
  if (ast.isDeploymentNode(parentRef)) {
138
- return stream(this.genUniqueDescedants(() => parentRef));
135
+ return stream(this.genUniqueDescedants(parentRef));
139
136
  }
140
137
  if (ast.isDeployedInstance(parentRef)) {
141
- // if (ast.isElement(target)) {
142
- return stream(this.genUniqueDescedants(() => {
143
- const target = parentRef.target.modelElement.value.ref;
144
- if (ast.isImported(target)) {
145
- return target.imported.ref;
146
- }
147
- return ast.isElement(target) ? target : undefined;
148
- }));
138
+ const target = parentRef.target.modelElement.value.ref;
139
+ // dprint-ignore
140
+ const resolvedTarget = ast.isImported(target)
141
+ ? target.imported.ref
142
+ : ast.isElement(target)
143
+ ? target
144
+ : undefined;
145
+ return stream(this.genUniqueDescedants(resolvedTarget));
149
146
  }
150
147
  if (ast.isElement(parentRef)) {
151
- return stream(this.genUniqueDescedants(() => parentRef));
148
+ return stream(this.genUniqueDescedants(parentRef));
152
149
  }
153
150
  return nonexhaustive(parentRef);
154
151
  }
@@ -13,17 +13,13 @@ export class IndexManager extends DefaultIndexManager {
13
13
  }
14
14
  projectElements(projectId, nodeType, uris) {
15
15
  const projects = this.services.workspace.ProjectsManager;
16
- const project = projects.getProject(projectId);
17
- const includePathStrings = project.includePaths?.map(uri => {
18
- const path = uri.toString();
19
- return path.endsWith('/') ? path : path + '/';
20
- }) ?? [];
21
16
  let documentUris = stream(this.symbolIndex.keys());
22
17
  return documentUris
23
18
  .filter(uri => {
24
- const belongsToProject = projects.belongsTo(uri) === projectId;
25
- const inIncludePath = includePathStrings.some(includePath => uri.startsWith(includePath));
26
- return (belongsToProject || inIncludePath) && (!uris || uris.has(uri));
19
+ if (uris && !uris.has(uri)) {
20
+ return false;
21
+ }
22
+ return projects.isIncluded(projectId, uri);
27
23
  })
28
24
  .flatMap(uri => this.getFileDescriptions(uri, nodeType));
29
25
  }
@@ -5,15 +5,21 @@ import { type LikeC4LangiumDocument } from '../ast';
5
5
  import type { LikeC4SharedServices } from '../module';
6
6
  export declare class LangiumDocuments extends DefaultLangiumDocuments {
7
7
  protected services: LikeC4SharedServices;
8
- protected compare: (a: string | undefined, b: string | undefined) => number;
9
8
  constructor(services: LikeC4SharedServices);
10
9
  addDocument(document: LangiumDocument): void;
11
10
  getDocument(uri: URI): LikeC4LangiumDocument | undefined;
12
11
  get all(): Stream<LikeC4LangiumDocument>;
13
12
  /**
14
- * Returns all user documents, excluding built-in documents.
13
+ * Returns all documents, excluding built-in documents and documents excluded by ProjectsManager.
15
14
  */
16
15
  get allExcludingBuiltin(): Stream<LikeC4LangiumDocument>;
16
+ /**
17
+ * Returns all documents for a project, including both project documents and documents included by the project.
18
+ */
17
19
  projectDocuments(projectId: ProjectId): Stream<LikeC4LangiumDocument>;
18
20
  groupedByProject(): Record<ProjectId, NonEmptyArray<LikeC4LangiumDocument>>;
21
+ /**
22
+ * Reset the project IDs of all documents.
23
+ */
24
+ resetProjectIds(): void;
19
25
  }
@@ -1,6 +1,6 @@
1
1
  import { compareNaturalHierarchically } from '@likec4/core/utils';
2
2
  import { DefaultLangiumDocuments, stream } from 'langium';
3
- import { groupBy, prop } from 'remeda';
3
+ import { hasAtLeast } from 'remeda';
4
4
  import { isLikeC4LangiumDocument } from '../ast';
5
5
  import { LikeC4LanguageMetaData } from '../generated/module';
6
6
  import { isLikeC4Builtin } from '../likec4lib';
@@ -14,7 +14,6 @@ const exclude = (doc) => {
14
14
  };
15
15
  export class LangiumDocuments extends DefaultLangiumDocuments {
16
16
  services;
17
- compare = compareNaturalHierarchically('/', true);
18
17
  constructor(services) {
19
18
  super(services);
20
19
  this.services = services;
@@ -34,7 +33,7 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
34
33
  getDocument(uri) {
35
34
  const doc = super.getDocument(uri);
36
35
  if (doc && !exclude(doc)) {
37
- doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
36
+ doc.likec4ProjectId = this.services.workspace.ProjectsManager.belongsTo(doc);
38
37
  }
39
38
  if (doc && !isLikeC4LangiumDocument(doc)) {
40
39
  throw new Error(`Document ${doc.uri.path} is not a LikeC4 document`);
@@ -46,7 +45,7 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
46
45
  .filter((doc) => {
47
46
  if (doc.textDocument.languageId === LikeC4LanguageMetaData.languageId) {
48
47
  if (!isLikeC4Builtin(doc.uri)) {
49
- doc.likec4ProjectId ??= this.services.workspace.ProjectsManager.belongsTo(doc);
48
+ doc.likec4ProjectId = this.services.workspace.ProjectsManager.belongsTo(doc);
50
49
  }
51
50
  return true;
52
51
  }
@@ -54,37 +53,46 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
54
53
  });
55
54
  }
56
55
  /**
57
- * Returns all user documents, excluding built-in documents.
56
+ * Returns all documents, excluding built-in documents and documents excluded by ProjectsManager.
58
57
  */
59
58
  get allExcludingBuiltin() {
60
59
  const projects = this.services.workspace.ProjectsManager;
61
- return super.all.filter((doc) => {
62
- // Exclude built-in and non-LikeC4 documents, and also documents excluded by ProjectsManager
63
- return !exclude(doc) && !projects.isExcluded(doc);
60
+ return this.all.filter((doc) => {
61
+ return !(isLikeC4Builtin(doc.uri) || projects.isExcluded(doc));
64
62
  });
65
63
  }
64
+ /**
65
+ * Returns all documents for a project, including both project documents and documents included by the project.
66
+ */
66
67
  projectDocuments(projectId) {
67
68
  const projects = this.services.workspace.ProjectsManager;
68
- const project = projects.getProject(projectId);
69
- const projectFolder = project.folderUri.toString() + (project.folderUri.path.endsWith('/') ? '' : '/');
70
- const includePathStrings = project.includePaths?.map(uri => {
71
- const path = uri.toString();
72
- return path.endsWith('/') ? path : path + '/';
73
- }) ?? [];
74
- return this.allExcludingBuiltin.filter(doc => {
75
- const docUri = doc.uri.toString();
76
- // Always include documents from the project's own folder
77
- if (docUri.startsWith(projectFolder)) {
78
- return true;
69
+ return this.all.filter(doc => {
70
+ if (isLikeC4Builtin(doc.uri)) {
71
+ return false;
79
72
  }
80
- // Check for addtional documents when the config has the `include:paths` property set.
81
- if (includePathStrings.length > 0) {
82
- return includePathStrings.some(includePath => docUri.startsWith(includePath));
83
- }
84
- return false;
73
+ return projects.isIncluded(projectId, doc.uri);
85
74
  });
86
75
  }
87
76
  groupedByProject() {
88
- return groupBy(this.allExcludingBuiltin.toArray(), prop('likec4ProjectId'));
77
+ return this.services.workspace.ProjectsManager
78
+ .all
79
+ .reduce((acc, projectId) => {
80
+ const docs = this.projectDocuments(projectId).toArray();
81
+ if (hasAtLeast(docs, 1)) {
82
+ acc[projectId] = docs;
83
+ }
84
+ return acc;
85
+ }, {});
86
+ }
87
+ /**
88
+ * Reset the project IDs of all documents.
89
+ */
90
+ resetProjectIds() {
91
+ super.all.forEach(doc => {
92
+ if (exclude(doc)) {
93
+ return;
94
+ }
95
+ delete doc.likec4ProjectId;
96
+ });
89
97
  }
90
98
  }
@@ -1,7 +1,8 @@
1
1
  import { type IncludeConfig, type LikeC4ProjectConfig, type LikeC4ProjectConfigInput } from '@likec4/config';
2
- import type { NonEmptyReadonlyArray } from '@likec4/core';
2
+ import type { NonEmptyArray, NonEmptyReadonlyArray } from '@likec4/core';
3
3
  import type { ProjectId, scalar } from '@likec4/core/types';
4
4
  import { type Cancellation, type LangiumDocument, URI, WorkspaceCache } from 'langium';
5
+ import picomatch from 'picomatch';
5
6
  import type { Tagged } from 'type-fest';
6
7
  import type { LikeC4SharedServices } from '../module';
7
8
  /**
@@ -15,12 +16,12 @@ interface ProjectData {
15
16
  config: LikeC4ProjectConfig;
16
17
  folder: ProjectFolder;
17
18
  folderUri: URI;
18
- exclude?: (path: string) => boolean;
19
+ exclude?: picomatch.Matcher;
19
20
  /**
20
21
  * Resolved include paths with both URI and folder string representations.
21
22
  * These are additional directories that are part of this project.
22
23
  */
23
- includePaths?: Array<{
24
+ includePaths?: NonEmptyArray<{
24
25
  uri: URI;
25
26
  folder: ProjectFolder;
26
27
  }>;
@@ -36,7 +37,7 @@ export interface Project {
36
37
  /**
37
38
  * Resolved include paths as URIs (if configured).
38
39
  */
39
- includePaths?: URI[];
40
+ includePaths?: NonEmptyReadonlyArray<URI>;
40
41
  }
41
42
  export declare class ProjectsManager {
42
43
  #private;
@@ -74,6 +75,12 @@ export declare class ProjectsManager {
74
75
  * Checks if the specified document should be excluded from processing.
75
76
  */
76
77
  isExcluded(document: LangiumDocument | URI | string): boolean;
78
+ /**
79
+ * Checks if the specified document is included by the project:
80
+ * - if the document belongs to the project and is not excluded
81
+ * - if the document is included by the project
82
+ */
83
+ isIncluded(projectId: ProjectId, document: LangiumDocument | URI | string): boolean;
77
84
  /**
78
85
  * Checks if it is a config file and it is not excluded by default exclude pattern
79
86
  *
@@ -83,7 +90,7 @@ export declare class ProjectsManager {
83
90
  /**
84
91
  * Registers likec4 project by config file.
85
92
  */
86
- registerConfigFile(configFile: URI): Promise<ProjectData>;
93
+ registerConfigFile(configFile: URI, cancelToken?: Cancellation.CancellationToken): Promise<ProjectData>;
87
94
  /**
88
95
  * Registers (or reloads) likec4 project by config file or config object.
89
96
  * If there is some project registered at same folder, it will be reloaded.
@@ -91,14 +98,14 @@ export declare class ProjectsManager {
91
98
  registerProject(opts: {
92
99
  config: LikeC4ProjectConfig | LikeC4ProjectConfigInput;
93
100
  folderUri: URI | string;
94
- }): Promise<ProjectData>;
101
+ }, cancelToken?: Cancellation.CancellationToken): Promise<ProjectData>;
95
102
  /**
96
103
  * Determines which project the given document belongs to.
97
104
  * If the document does not belong to any project, returns the default project ID.
98
105
  */
99
106
  belongsTo(document: LangiumDocument | URI | string): scalar.ProjectId;
100
- reloadProjects(): Promise<void>;
101
- protected _reloadProjects(): Promise<void>;
107
+ reloadProjects(cancelToken?: Cancellation.CancellationToken): Promise<void>;
108
+ protected _reloadProjects(cancelToken?: Cancellation.CancellationToken): Promise<void>;
102
109
  protected uniqueProjectId(name: string): scalar.ProjectId;
103
110
  protected reset(): void;
104
111
  rebuidProject(projectId: ProjectId, cancelToken?: Cancellation.CancellationToken): Promise<void>;