@likec4/language-server 1.46.3 → 1.47.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 (53) hide show
  1. package/dist/LikeC4LanguageServices.d.ts +27 -21
  2. package/dist/LikeC4LanguageServices.js +24 -14
  3. package/dist/Rpc.js +9 -3
  4. package/dist/ast.d.ts +1 -1
  5. package/dist/browser.d.ts +0 -1
  6. package/dist/browser.js +0 -1
  7. package/dist/bundled.mjs +3773 -3536
  8. package/dist/filesystem/ChokidarWatcher.d.ts +3 -0
  9. package/dist/filesystem/ChokidarWatcher.js +67 -42
  10. package/dist/filesystem/LikeC4FileSystem.d.ts +1 -1
  11. package/dist/filesystem/LikeC4FileSystem.js +16 -6
  12. package/dist/generated/ast.d.ts +2 -2
  13. package/dist/generated/ast.js +3 -3
  14. package/dist/generated/grammar.js +1 -1
  15. package/dist/generated-lib/icons.js +7 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/lsp/CodeLensProvider.js +1 -1
  19. package/dist/lsp/CompletionProvider.d.ts +4 -2
  20. package/dist/lsp/CompletionProvider.js +41 -3
  21. package/dist/lsp/DocumentSymbolProvider.js +1 -1
  22. package/dist/lsp/SemanticTokenProvider.d.ts +8 -1
  23. package/dist/lsp/SemanticTokenProvider.js +52 -11
  24. package/dist/mcp/interfaces.d.ts +1 -1
  25. package/dist/mcp/interfaces.js +0 -1
  26. package/dist/mcp/server/StreamableLikeC4MCPServer.js +27 -51
  27. package/dist/mcp/tools/_common.d.ts +2 -2
  28. package/dist/mcp/tools/find-relationships.d.ts +195 -5
  29. package/dist/mcp/tools/list-projects.d.ts +191 -3
  30. package/dist/mcp/tools/open-view.d.ts +194 -4
  31. package/dist/mcp/tools/read-deployment.d.ts +194 -4
  32. package/dist/mcp/tools/read-element.d.ts +194 -4
  33. package/dist/mcp/tools/read-project-summary.d.ts +193 -3
  34. package/dist/mcp/tools/read-view.d.ts +194 -4
  35. package/dist/mcp/tools/search-element.d.ts +193 -3
  36. package/dist/model/model-builder.d.ts +4 -2
  37. package/dist/model/model-builder.js +58 -57
  38. package/dist/model/model-parser.d.ts +6 -6
  39. package/dist/model/parser/Base.js +58 -48
  40. package/dist/model/parser/GlobalsParser.d.ts +3 -3
  41. package/dist/model/parser/ViewsParser.js +2 -2
  42. package/dist/protocol.d.ts +5 -0
  43. package/dist/references/scope-provider.d.ts +1 -1
  44. package/dist/references/scope-provider.js +18 -21
  45. package/dist/utils/elementRef.js +10 -4
  46. package/dist/validation/index.d.ts +1 -1
  47. package/dist/workspace/IndexManager.js +4 -2
  48. package/dist/workspace/LangiumDocuments.d.ts +4 -0
  49. package/dist/workspace/LangiumDocuments.js +26 -9
  50. package/dist/workspace/ProjectsManager.d.ts +9 -3
  51. package/dist/workspace/ProjectsManager.js +141 -102
  52. package/package.json +16 -15
  53. package/lib/package.json +0 -159
@@ -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
  }
@@ -3,11 +3,17 @@ import { ast } from '../ast';
3
3
  * Returns referenced AST Element
4
4
  */
5
5
  export function elementRef(node) {
6
- let el = ast.isStrictFqnElementRef(node) ? node.el.ref : node.modelElement.value.ref;
7
- if (el?.$type === 'Imported') {
8
- el = el.imported.ref;
6
+ try {
7
+ let el = ast.isStrictFqnElementRef(node) ? node.el.ref : node.modelElement.value.ref;
8
+ if (el?.$type === 'Imported') {
9
+ el = el.imported.ref;
10
+ }
11
+ return el?.$type === 'Element' ? el : undefined;
12
+ }
13
+ catch {
14
+ // ignore reference errors
15
+ return undefined;
9
16
  }
10
- return el?.$type === 'Element' ? el : undefined;
11
17
  }
12
18
  /**
13
19
  * Returns FQN of StrictFqnElementRef
@@ -4,7 +4,7 @@ import type { LikeC4Services } from '../module';
4
4
  export { LikeC4DocumentValidator } from './DocumentValidator';
5
5
  type Guard<N extends AstNode> = (n: AstNode) => n is N;
6
6
  type Guarded<G> = G extends Guard<infer N> ? N : never;
7
- declare const isValidatableAstNode: (n: AstNode) => n is ast.DynamicViewDisplayVariantProperty | ast.LinkProperty | ast.ViewStringProperty | ast.ElementStringProperty | ast.ElementStyleProperty | ast.IconProperty | ast.MetadataBody | ast.RelationStringProperty | ast.MetadataAttribute | ast.NotationProperty | ast.NotesProperty | ast.SpecificationElementStringProperty | ast.SpecificationRelationshipStringProperty | ast.HexColor | ast.RGBAColor | ast.DeployedInstance | ast.DeploymentNode | ast.DeploymentViewRulePredicate | ast.DeploymentViewRuleStyle | ast.ViewRuleAutoLayout | ast.DynamicViewGlobalPredicateRef | ast.DynamicViewIncludePredicate | ast.ViewRuleGlobalStyle | ast.ViewRuleStyle | ast.DynamicStepChain | ast.DynamicStepSingle | ast.ElementKindExpression | ast.ElementTagExpression | ast.FqnRefExpr | ast.WildcardExpression | ast.FqnExprWhere | ast.FqnExprWith | ast.DirectedRelationExpr | ast.InOutRelationExpr | ast.IncomingRelationExpr | ast.OutgoingRelationExpr | ast.RelationExprWhere | ast.RelationExprWith | ast.Element | ast.ExtendDeployment | ast.ExtendElement | ast.Imported | ast.DeploymentView | ast.DynamicView | ast.ElementView | ast.ArrowProperty | ast.ColorProperty | ast.LineProperty | ast.PaddingSizeProperty | ast.ShapeSizeProperty | ast.TextSizeProperty | ast.BorderProperty | ast.MultipleProperty | ast.OpacityProperty | ast.ShapeProperty | ast.ViewRuleGlobalPredicateRef | ast.ViewRuleGroup | ast.ViewRulePredicate | ast.ViewRuleRank | ast.DynamicViewParallelSteps | ast.SpecificationRelationshipKind | ast.SpecificationRule | ast.ElementRef | ast.DeploymentRelation | ast.Relation | ast.GlobalDynamicPredicateGroup | ast.Globals | ast.GlobalPredicateGroup | ast.GlobalStyle | ast.SpecificationDeploymentNodeKind | ast.SpecificationElementKind | ast.GlobalStyleGroup | ast.SpecificationColor | ast.NavigateToProperty | ast.Tags | ast.ExtendRelation | ast.ImportsFromPoject | ast.SpecificationTag;
7
+ declare const isValidatableAstNode: (n: AstNode) => n is ast.Element | ast.DeployedInstance | ast.DeploymentNode | ast.Tags | ast.ElementKindExpression | ast.ElementTagExpression | ast.FqnRefExpr | ast.WildcardExpression | ast.FqnExprWhere | ast.FqnExprWith | ast.DirectedRelationExpr | ast.InOutRelationExpr | ast.IncomingRelationExpr | ast.OutgoingRelationExpr | ast.RelationExprWhere | ast.RelationExprWith | ast.ImportsFromPoject | ast.Imported | ast.Globals | ast.GlobalPredicateGroup | ast.GlobalDynamicPredicateGroup | ast.GlobalStyle | ast.GlobalStyleGroup | ast.DeploymentViewRulePredicate | ast.DynamicViewIncludePredicate | ast.ViewRulePredicate | ast.DeploymentView | ast.DynamicView | ast.ViewRuleGroup | ast.ElementView | ast.DeploymentViewRuleStyle | ast.ViewRuleRank | ast.ViewRuleStyle | ast.DynamicViewParallelSteps | ast.DynamicStepChain | ast.DynamicStepSingle | ast.ViewRuleAutoLayout | ast.LinkProperty | ast.ViewStringProperty | ast.SpecificationDeploymentNodeKind | ast.SpecificationElementKind | ast.ExtendDeployment | ast.DeploymentRelation | ast.ExtendElement | ast.ExtendRelation | ast.Relation | ast.SpecificationRule | ast.BorderProperty | ast.ColorProperty | ast.IconProperty | ast.MultipleProperty | ast.OpacityProperty | ast.PaddingSizeProperty | ast.ShapeProperty | ast.ShapeSizeProperty | ast.TextSizeProperty | ast.ElementStyleProperty | ast.SpecificationRelationshipKind | ast.ViewRuleGlobalPredicateRef | ast.ViewRuleGlobalStyle | ast.DynamicViewGlobalPredicateRef | ast.ArrowProperty | ast.LineProperty | ast.DynamicViewDisplayVariantProperty | ast.MetadataBody | ast.ElementStringProperty | ast.MetadataAttribute | ast.NotationProperty | ast.NotesProperty | ast.RelationStringProperty | ast.SpecificationElementStringProperty | ast.SpecificationRelationshipStringProperty | ast.NavigateToProperty | ast.ElementRef | ast.SpecificationTag | ast.SpecificationColor | ast.HexColor | ast.RGBAColor;
8
8
  type ValidatableAstNode = Guarded<typeof isValidatableAstNode>;
9
9
  export declare function checksFromDiagnostics(doc: LikeC4LangiumDocument): {
10
10
  isValid: (n: ValidatableAstNode) => boolean;
@@ -16,8 +16,10 @@ export class IndexManager extends DefaultIndexManager {
16
16
  let documentUris = stream(this.symbolIndex.keys());
17
17
  return documentUris
18
18
  .filter(uri => {
19
- return (!uris || uris.has(uri)) && (projects.belongsTo(uri) === projectId ||
20
- projects.isIncluded(projectId, uri));
19
+ if (uris && !uris.has(uri)) {
20
+ return false;
21
+ }
22
+ return projects.isIncluded(projectId, uri);
21
23
  })
22
24
  .flatMap(uri => this.getFileDescriptions(uri, nodeType));
23
25
  }
@@ -8,6 +8,10 @@ export declare class LangiumDocuments extends DefaultLangiumDocuments {
8
8
  constructor(services: LikeC4SharedServices);
9
9
  addDocument(document: LangiumDocument): void;
10
10
  getDocument(uri: URI): LikeC4LangiumDocument | undefined;
11
+ /**
12
+ * Returns all known documents, without any filtering.
13
+ */
14
+ get allKnownDocuments(): Stream<LangiumDocument>;
11
15
  get all(): Stream<LikeC4LangiumDocument>;
12
16
  /**
13
17
  * Returns all documents, excluding built-in documents and documents excluded by ProjectsManager.
@@ -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';
@@ -32,16 +32,22 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
32
32
  }
33
33
  getDocument(uri) {
34
34
  const doc = super.getDocument(uri);
35
- if (doc && !exclude(doc)) {
36
- doc.likec4ProjectId = this.services.workspace.ProjectsManager.belongsTo(doc);
37
- }
38
35
  if (doc && !isLikeC4LangiumDocument(doc)) {
39
36
  throw new Error(`Document ${doc.uri.path} is not a LikeC4 document`);
40
37
  }
38
+ if (doc && !exclude(doc)) {
39
+ doc.likec4ProjectId = this.services.workspace.ProjectsManager.belongsTo(doc);
40
+ }
41
41
  return doc;
42
42
  }
43
+ /**
44
+ * Returns all known documents, without any filtering.
45
+ */
46
+ get allKnownDocuments() {
47
+ return stream(this.documentMap.values());
48
+ }
43
49
  get all() {
44
- return stream(this.documentMap.values())
50
+ return this.allKnownDocuments
45
51
  .filter((doc) => {
46
52
  if (doc.textDocument.languageId === LikeC4LanguageMetaData.languageId) {
47
53
  if (!isLikeC4Builtin(doc.uri)) {
@@ -66,18 +72,29 @@ export class LangiumDocuments extends DefaultLangiumDocuments {
66
72
  */
67
73
  projectDocuments(projectId) {
68
74
  const projects = this.services.workspace.ProjectsManager;
69
- return this.allExcludingBuiltin.filter(doc => {
70
- return doc.likec4ProjectId === projectId || projects.isIncluded(projectId, doc.uri);
75
+ return this.all.filter(doc => {
76
+ if (isLikeC4Builtin(doc.uri)) {
77
+ return false;
78
+ }
79
+ return projects.isIncluded(projectId, doc.uri);
71
80
  });
72
81
  }
73
82
  groupedByProject() {
74
- return groupBy(this.allExcludingBuiltin.toArray(), prop('likec4ProjectId'));
83
+ return this.services.workspace.ProjectsManager
84
+ .all
85
+ .reduce((acc, projectId) => {
86
+ const docs = this.projectDocuments(projectId).toArray();
87
+ if (hasAtLeast(docs, 1)) {
88
+ acc[projectId] = docs;
89
+ }
90
+ return acc;
91
+ }, {});
75
92
  }
76
93
  /**
77
94
  * Reset the project IDs of all documents.
78
95
  */
79
96
  resetProjectIds() {
80
- super.all.forEach(doc => {
97
+ this.allKnownDocuments.forEach(doc => {
81
98
  if (exclude(doc)) {
82
99
  return;
83
100
  }
@@ -2,6 +2,7 @@ import { type IncludeConfig, type LikeC4ProjectConfig, type LikeC4ProjectConfigI
2
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,7 +16,7 @@ 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.
@@ -59,6 +60,10 @@ export declare class ProjectsManager {
59
60
  get default(): ProjectData;
60
61
  get all(): NonEmptyReadonlyArray<scalar.ProjectId>;
61
62
  getProject(arg: scalar.ProjectId | LangiumDocument): Project;
63
+ /**
64
+ * Returns all projects that include the specified folder, or inside the folder.
65
+ */
66
+ findAllProjectsByFolder(folder: URI | string): ProjectData[];
62
67
  /**
63
68
  * Validates and ensures the project ID.
64
69
  * If no project ID is specified, returns default project ID
@@ -75,10 +80,11 @@ export declare class ProjectsManager {
75
80
  */
76
81
  isExcluded(document: LangiumDocument | URI | string): boolean;
77
82
  /**
78
- * Checks if the specified document is included by the project.
83
+ * Checks if the specified document is included by the project:
84
+ * - if the document belongs to the project and is not excluded
85
+ * - if the document is included by the project
79
86
  */
80
87
  isIncluded(projectId: ProjectId, document: LangiumDocument | URI | string): boolean;
81
- includedInProjects(document: LangiumDocument | URI | string): ProjectId[];
82
88
  /**
83
89
  * Checks if it is a config file and it is not excluded by default exclude pattern
84
90
  *
@@ -1,11 +1,13 @@
1
+ var _a;
1
2
  import { isLikeC4Config, normalizeIncludeConfig, validateProjectConfig, } from '@likec4/config';
2
- import { BiMap, invariant, memoizeProp, nonNullable } from '@likec4/core/utils';
3
- import { wrapError } from '@likec4/log';
3
+ import { BiMap, compareNaturalHierarchically, DefaultMap, invariant, memoizeProp, nonNullable, } from '@likec4/core/utils';
4
+ import { loggable, wrapError } from '@likec4/log';
4
5
  import { deepEqual } from 'fast-equals';
5
- import { interruptAndCheck, URI, WorkspaceCache, } from 'langium';
6
+ import { isOperationCancelled, OperationCancelled, URI, WorkspaceCache, } from 'langium';
6
7
  import picomatch from 'picomatch';
7
- import { filter, hasAtLeast, isNullish, map, pipe, prop, sortBy } from 'remeda';
8
- import { joinRelativeURL, parseFilename, withoutProtocol, withTrailingSlash, } from 'ufo';
8
+ import { filter, hasAtLeast, isNullish, map, pipe, prop, sort } from 'remeda';
9
+ import { cleanDoubleSlashes, isRelative, joinRelativeURL, joinURL, parseFilename, withoutProtocol, withoutTrailingSlash, withTrailingSlash, } from 'ufo';
10
+ import { isLikeC4Builtin } from '../likec4lib';
9
11
  import { logger as mainLogger } from '../logger';
10
12
  const logger = mainLogger.getChild('ProjectsManager');
11
13
  function normalizeUri(uri) {
@@ -20,6 +22,11 @@ function normalizeUri(uri) {
20
22
  return uri.uri.toString();
21
23
  }
22
24
  }
25
+ /**
26
+ * Compare function to ensure consistent order
27
+ */
28
+ const compare = compareNaturalHierarchically('/', true);
29
+ const compareUri = (a, b) => compare(withoutTrailingSlash(a.path), withoutTrailingSlash(b.path));
23
30
  /**
24
31
  * Returns a predicate that checks if the given path is included in the project folder.
25
32
  */
@@ -66,6 +73,23 @@ export class ProjectsManager {
66
73
  * This ensures that the most specific project is used for a document.
67
74
  */
68
75
  #projects = [];
76
+ /**
77
+ * This is a cached lookup for performance.
78
+ */
79
+ #lookupById = new DefaultMap((id) => {
80
+ if (id === _a.DefaultProjectId) {
81
+ const folder = ProjectFolder(this.getWorkspaceFolder());
82
+ return {
83
+ id,
84
+ config: DefaultProject.config,
85
+ folder,
86
+ folderUri: URI.parse(folder),
87
+ exclude: DefaultProject.exclude,
88
+ includeConfig: DefaultProject.includeConfig,
89
+ };
90
+ }
91
+ return nonNullable(this.#projects.find(p => p.id === id), `Project ${id} not found`);
92
+ });
69
93
  #excludedDocuments = new WeakMap();
70
94
  constructor(services) {
71
95
  this.services = services;
@@ -85,14 +109,14 @@ export class ProjectsManager {
85
109
  if (this.#projects.length > 1) {
86
110
  return undefined;
87
111
  }
88
- return this.#projects[0]?.id ?? ProjectsManager.DefaultProjectId;
112
+ return this.#projects[0]?.id ?? _a.DefaultProjectId;
89
113
  }
90
114
  set defaultProjectId(id) {
91
115
  if (id === this.#defaultProjectId) {
92
116
  return;
93
117
  }
94
118
  this.#defaultProject = undefined;
95
- if (!id || id === ProjectsManager.DefaultProjectId) {
119
+ if (!id || id === _a.DefaultProjectId) {
96
120
  logger.debug `reset default project ID`;
97
121
  this.#defaultProjectId = undefined;
98
122
  return;
@@ -103,20 +127,8 @@ export class ProjectsManager {
103
127
  }
104
128
  get default() {
105
129
  if (!this.#defaultProject) {
106
- const id = this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
107
- let project = this.#projects.find(p => p.id === id);
108
- if (!project) {
109
- const folderUri = this.getWorkspaceFolder();
110
- project = {
111
- id,
112
- config: DefaultProject.config,
113
- folder: ProjectFolder(folderUri),
114
- folderUri,
115
- exclude: DefaultProject.exclude,
116
- includeConfig: DefaultProject.includeConfig,
117
- };
118
- }
119
- this.#defaultProject = project;
130
+ const id = this.defaultProjectId ?? _a.DefaultProjectId;
131
+ this.#defaultProject = this.#lookupById.get(id);
120
132
  }
121
133
  return this.#defaultProject;
122
134
  }
@@ -140,23 +152,7 @@ export class ProjectsManager {
140
152
  }
141
153
  getProject(arg) {
142
154
  const id = typeof arg === 'string' ? arg : (arg.likec4ProjectId || this.belongsTo(arg));
143
- if (id === DefaultProject.id) {
144
- let folderUri;
145
- try {
146
- folderUri = this.services.workspace.WorkspaceManager.workspaceUri;
147
- }
148
- catch (error) {
149
- logger.warn('Failed to get workspace URI, using default folder', { error });
150
- folderUri = URI.file('/');
151
- // ignore - workspace not initialized
152
- }
153
- return {
154
- id: ProjectsManager.DefaultProjectId,
155
- config: DefaultProject.config,
156
- folderUri,
157
- };
158
- }
159
- const project = nonNullable(this.#projects.find(p => p.id === id), `Project "${id}" not found`);
155
+ const project = this.#lookupById.get(id);
160
156
  return {
161
157
  id,
162
158
  folderUri: project.folderUri,
@@ -164,14 +160,22 @@ export class ProjectsManager {
164
160
  ...(project.includePaths && { includePaths: map(project.includePaths, prop('uri')) }),
165
161
  };
166
162
  }
163
+ /**
164
+ * Returns all projects that include the specified folder, or inside the folder.
165
+ */
166
+ findAllProjectsByFolder(folder) {
167
+ const projectFolder = ProjectFolder(folder);
168
+ const isInsideOrIncludes = (p) => p.folder.startsWith(projectFolder) || projectFolder.startsWith(p.folder);
169
+ return this.#projects.filter(isInsideOrIncludes);
170
+ }
167
171
  /**
168
172
  * Validates and ensures the project ID.
169
173
  * If no project ID is specified, returns default project ID
170
174
  * If there are multiple projects and default project is not set, throws an error
171
175
  */
172
176
  ensureProjectId(projectId) {
173
- if (projectId === ProjectsManager.DefaultProjectId) {
174
- return this.defaultProjectId ?? ProjectsManager.DefaultProjectId;
177
+ if (projectId === _a.DefaultProjectId) {
178
+ return this.defaultProjectId ?? _a.DefaultProjectId;
175
179
  }
176
180
  if (projectId) {
177
181
  invariant(this.#projectIdToFolder.has(projectId), `Project ID ${projectId} is not registered`);
@@ -196,7 +200,11 @@ export class ProjectsManager {
196
200
  if (typeof document === 'string' || URI.isUri(document)) {
197
201
  let docUriAsString = normalizeUri(document);
198
202
  const project = this.findProjectForDocument(docUriAsString);
199
- return !!project.exclude && project.exclude(withoutProtocol(docUriAsString));
203
+ if (!project.exclude) {
204
+ return false;
205
+ }
206
+ const input = withoutProtocol(docUriAsString);
207
+ return project.exclude(input);
200
208
  }
201
209
  let isExcluded = this.#excludedDocuments.get(document);
202
210
  if (isExcluded === undefined) {
@@ -206,24 +214,23 @@ export class ProjectsManager {
206
214
  return isExcluded;
207
215
  }
208
216
  /**
209
- * Checks if the specified document is included by the project.
217
+ * Checks if the specified document is included by the project:
218
+ * - if the document belongs to the project and is not excluded
219
+ * - if the document is included by the project
210
220
  */
211
221
  isIncluded(projectId, document) {
212
- if (typeof document === 'string' || URI.isUri(document)) {
213
- let project = this.#projects.find(p => p.id === projectId);
214
- if (!project || !project.includePaths) {
215
- return false;
216
- }
217
- return project.includePaths.some(isParentFolderFor(document));
222
+ if (typeof document !== 'string' && !URI.isUri(document)) {
223
+ return this.isIncluded(projectId, document.uri);
218
224
  }
219
- return this.isIncluded(projectId, document.uri);
220
- }
221
- includedInProjects(document) {
222
- if (typeof document === 'string' || URI.isUri(document)) {
223
- let docUriAsString = normalizeUri(document);
224
- return pipe(this.#projects, filter(p => !!p.includePaths && p.includePaths.some(isParentFolderFor(docUriAsString))), map(p => p.id));
225
+ const belongsTo = this.belongsTo(document);
226
+ if (belongsTo === projectId) {
227
+ return !this.isExcluded(document);
228
+ }
229
+ let includePaths = this.#lookupById.get(projectId)?.includePaths;
230
+ if (!includePaths) {
231
+ return false;
225
232
  }
226
- return this.includedInProjects(document.uri);
233
+ return includePaths.some(isParentFolderFor(document));
227
234
  }
228
235
  /**
229
236
  * Checks if it is a config file and it is not excluded by default exclude pattern
@@ -258,8 +265,11 @@ export class ProjectsManager {
258
265
  return await this.registerProject({ config, folderUri }, cancelToken);
259
266
  }
260
267
  catch (error) {
261
- this.services.lsp.Connection?.window.showErrorMessage(`LikeC4: Failed to register project at ${configFile.fsPath}`);
262
- throw wrapError(error, `Failed to register project config ${configFile.fsPath}:\n`);
268
+ if (!isOperationCancelled(error)) {
269
+ this.services.lsp.Connection?.window.showErrorMessage(`LikeC4: Failed to register project at ${configFile.fsPath}`);
270
+ throw wrapError(error, `Failed to register project config ${configFile.fsPath}:\n`);
271
+ }
272
+ return Promise.reject(error);
263
273
  }
264
274
  }
265
275
  /**
@@ -291,9 +301,7 @@ export class ProjectsManager {
291
301
  // if there is any project within subfolder or parent folder
292
302
  // we need to reset assigned to documents project IDs
293
303
  mustReset = this.#projects.some(p => p.folder.startsWith(folder) || folder.startsWith(p.folder));
294
- this.#projects = pipe([...this.#projects, project], sortBy(
295
- // sort by folder depth (longest first)
296
- [({ folder }) => withoutProtocol(folder).split('/').length, 'desc']));
304
+ this.#projects = pipe([...this.#projects, project], sort((a, b) => compareUri(a.folderUri, b.folderUri)));
297
305
  logger.info `register project ${project.id} folder: ${folder}`;
298
306
  }
299
307
  else {
@@ -314,13 +322,20 @@ export class ProjectsManager {
314
322
  const includeConfig = normalizeIncludeConfig(config.include);
315
323
  project.includeConfig = includeConfig;
316
324
  }
317
- // Reset cached default project
318
- this.#defaultProject = undefined;
319
325
  if (isNullish(config.exclude)) {
320
326
  project.exclude = DefaultProject.exclude;
321
327
  }
322
328
  else if (hasAtLeast(config.exclude, 1)) {
323
- project.exclude = picomatch(config.exclude, { dot: true });
329
+ const patterns = map(config.exclude, p => {
330
+ if (!isRelative(p) && !p.startsWith('**')) {
331
+ p = joinURL('**', p);
332
+ }
333
+ return cleanDoubleSlashes(joinRelativeURL(project.folderUri.path, p));
334
+ });
335
+ project.exclude = picomatch(patterns, {
336
+ contains: true,
337
+ dot: true,
338
+ });
324
339
  }
325
340
  // Resolve include paths relative to project folder
326
341
  if (project.includeConfig.paths && hasAtLeast(project.includeConfig.paths, 1)) {
@@ -366,14 +381,22 @@ export class ProjectsManager {
366
381
  else {
367
382
  delete project.includePaths;
368
383
  }
384
+ // Reset cached default project
385
+ this.#defaultProject = undefined;
369
386
  this.#projectIdToFolder.set(project.id, folder);
387
+ this.#lookupById.clear();
370
388
  // Reset assigned project IDs if no projects reload is active
371
389
  if (mustReset && !this.#activeReload) {
372
390
  await this.rebuidProject(project.id, cancelToken).catch(error => {
391
+ if (isOperationCancelled(error)) {
392
+ return Promise.reject(error);
393
+ }
373
394
  logger.warn('Failed to rebuild project {projectId} after config change', {
374
395
  projectId: project.id,
375
396
  error,
376
397
  });
398
+ // ignore error, we logged it
399
+ return Promise.resolve();
377
400
  });
378
401
  }
379
402
  return project;
@@ -393,19 +416,25 @@ export class ProjectsManager {
393
416
  }
394
417
  #activeReload = null;
395
418
  async reloadProjects(cancelToken) {
396
- try {
397
- if (!this.#activeReload) {
398
- logger.debug `schedule reload projects`;
399
- this.#activeReload = this._reloadProjects(cancelToken);
400
- }
401
- else {
402
- logger.debug `reload projects is already in progress, waiting`;
403
- }
404
- await this.#activeReload;
419
+ if (this.#activeReload) {
420
+ logger.debug `reload projects is already in progress, waiting`;
421
+ return await this.#activeReload.catch(() => {
422
+ // ignore errors
423
+ });
405
424
  }
406
- finally {
425
+ logger.debug `schedule reload projects`;
426
+ this.#activeReload = Promise.resolve()
427
+ .then(() => this._reloadProjects(cancelToken))
428
+ .catch(error => {
429
+ if (!isOperationCancelled(error)) {
430
+ logger.warn('Failed to reload projects', { error });
431
+ }
432
+ return Promise.reject(error);
433
+ })
434
+ .finally(() => {
407
435
  this.#activeReload = null;
408
- }
436
+ });
437
+ return await this.#activeReload;
409
438
  }
410
439
  async _reloadProjects(cancelToken) {
411
440
  const folders = this.services.workspace.WorkspaceManager.workspaceFolders;
@@ -433,20 +462,25 @@ export class ProjectsManager {
433
462
  if (configFiles.length === 0 && this.#projects.length !== 0) {
434
463
  logger.warning('No config files found, but some projects were registered before');
435
464
  }
465
+ // Sort config files hierarchically, ensuring consistent order
466
+ configFiles.sort(compareUri);
436
467
  this.#projects = [];
437
468
  this.#projectIdToFolder.clear();
469
+ this.#lookupById.clear();
438
470
  for (const uri of configFiles) {
439
- if (cancelToken) {
440
- await interruptAndCheck(cancelToken);
441
- }
442
- try {
443
- await this.registerConfigFile(uri, cancelToken);
444
- }
445
- catch (error) {
446
- logger.error('Failed to load config file {uri}', { uri: uri.fsPath, error });
471
+ if (cancelToken?.isCancellationRequested) {
472
+ break;
447
473
  }
474
+ await this.registerConfigFile(uri, cancelToken).catch(error => {
475
+ if (!isOperationCancelled(error)) {
476
+ logger.warn(loggable(error));
477
+ }
478
+ });
448
479
  }
449
480
  this.reset();
481
+ if (cancelToken?.isCancellationRequested) {
482
+ throw OperationCancelled;
483
+ }
450
484
  await this.services.workspace.WorkspaceManager.rebuildAll(cancelToken);
451
485
  }
452
486
  uniqueProjectId(name) {
@@ -466,6 +500,7 @@ export class ProjectsManager {
466
500
  this.services.workspace.LangiumDocuments.resetProjectIds();
467
501
  this.documentBelongsTo.clear();
468
502
  this.mappingsToProject.clear();
503
+ this.#lookupById.clear();
469
504
  this.#excludedDocuments = new WeakMap();
470
505
  }
471
506
  async rebuidProject(projectId, cancelToken) {
@@ -478,9 +513,15 @@ export class ProjectsManager {
478
513
  const log = logger.getChild(project.id);
479
514
  const folder = project.folder;
480
515
  const includePaths = project.includePaths;
481
- const docs = this.services.workspace.LangiumDocuments
482
- .all
483
- .filter(doc => {
516
+ const allDocs = this.services.workspace.LangiumDocuments
517
+ .allKnownDocuments
518
+ .filter(doc => !isLikeC4Builtin(doc.uri))
519
+ .toArray();
520
+ // If no documents are found, return early
521
+ if (allDocs.length === 0) {
522
+ return;
523
+ }
524
+ const docs = pipe(allDocs, filter(doc => {
484
525
  if (project.exclude?.(doc.uri.path)) {
485
526
  return false;
486
527
  }
@@ -493,25 +534,22 @@ export class ProjectsManager {
493
534
  }
494
535
  const docdir = withTrailingSlash(joinRelativeURL(docUriStr, '..'));
495
536
  return docdir.startsWith(folder) || folder.startsWith(docdir);
496
- })
497
- .map(d => d.uri)
498
- .toArray();
499
- if (docs.length > 0) {
500
- log.info('rebuild project documents: {docs}', {
501
- docs: docs.length,
502
- });
503
- this.reset();
504
- await this.services.workspace.DocumentBuilder
505
- .update(docs, [], cancelToken)
506
- .catch(error => {
507
- log.warn('Failed to rebuild project', {
508
- error,
509
- });
510
- });
511
- }
512
- else {
537
+ }), map(d => d.uri));
538
+ if (docs.length === 0) {
513
539
  log.debug('no documents in project, skipping rebuild');
540
+ return;
514
541
  }
542
+ log.info('rebuild project documents: {docs}', {
543
+ docs: docs.length,
544
+ });
545
+ this.reset();
546
+ await this.services.workspace.DocumentBuilder
547
+ .update(docs, [], cancelToken)
548
+ .catch(error => {
549
+ log.warn('Failed to rebuild project', {
550
+ error,
551
+ });
552
+ });
515
553
  }
516
554
  findProjectForDocument(documentUri) {
517
555
  return this.mappingsToProject.get(documentUri, () => {
@@ -569,3 +607,4 @@ export class ProjectsManager {
569
607
  }
570
608
  }
571
609
  }
610
+ _a = ProjectsManager;