@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.
- package/dist/LikeC4LanguageServices.d.ts +27 -21
- package/dist/LikeC4LanguageServices.js +24 -14
- package/dist/Rpc.js +9 -3
- package/dist/ast.d.ts +1 -1
- package/dist/browser.d.ts +0 -1
- package/dist/browser.js +0 -1
- package/dist/bundled.mjs +3773 -3536
- package/dist/filesystem/ChokidarWatcher.d.ts +3 -0
- package/dist/filesystem/ChokidarWatcher.js +67 -42
- package/dist/filesystem/LikeC4FileSystem.d.ts +1 -1
- package/dist/filesystem/LikeC4FileSystem.js +16 -6
- package/dist/generated/ast.d.ts +2 -2
- package/dist/generated/ast.js +3 -3
- package/dist/generated/grammar.js +1 -1
- package/dist/generated-lib/icons.js +7 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/lsp/CodeLensProvider.js +1 -1
- package/dist/lsp/CompletionProvider.d.ts +4 -2
- package/dist/lsp/CompletionProvider.js +41 -3
- package/dist/lsp/DocumentSymbolProvider.js +1 -1
- package/dist/lsp/SemanticTokenProvider.d.ts +8 -1
- package/dist/lsp/SemanticTokenProvider.js +52 -11
- package/dist/mcp/interfaces.d.ts +1 -1
- package/dist/mcp/interfaces.js +0 -1
- package/dist/mcp/server/StreamableLikeC4MCPServer.js +27 -51
- package/dist/mcp/tools/_common.d.ts +2 -2
- package/dist/mcp/tools/find-relationships.d.ts +195 -5
- package/dist/mcp/tools/list-projects.d.ts +191 -3
- package/dist/mcp/tools/open-view.d.ts +194 -4
- package/dist/mcp/tools/read-deployment.d.ts +194 -4
- package/dist/mcp/tools/read-element.d.ts +194 -4
- package/dist/mcp/tools/read-project-summary.d.ts +193 -3
- package/dist/mcp/tools/read-view.d.ts +194 -4
- package/dist/mcp/tools/search-element.d.ts +193 -3
- package/dist/model/model-builder.d.ts +4 -2
- package/dist/model/model-builder.js +58 -57
- package/dist/model/model-parser.d.ts +6 -6
- package/dist/model/parser/Base.js +58 -48
- package/dist/model/parser/GlobalsParser.d.ts +3 -3
- package/dist/model/parser/ViewsParser.js +2 -2
- package/dist/protocol.d.ts +5 -0
- package/dist/references/scope-provider.d.ts +1 -1
- package/dist/references/scope-provider.js +18 -21
- package/dist/utils/elementRef.js +10 -4
- package/dist/validation/index.d.ts +1 -1
- package/dist/workspace/IndexManager.js +4 -2
- package/dist/workspace/LangiumDocuments.d.ts +4 -0
- package/dist/workspace/LangiumDocuments.js +26 -9
- package/dist/workspace/ProjectsManager.d.ts +9 -3
- package/dist/workspace/ProjectsManager.js +141 -102
- package/package.json +16 -15
- 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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
135
|
+
return stream(this.genUniqueDescedants(parentRef));
|
|
139
136
|
}
|
|
140
137
|
if (ast.isDeployedInstance(parentRef)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
148
|
+
return stream(this.genUniqueDescedants(parentRef));
|
|
152
149
|
}
|
|
153
150
|
return nonexhaustive(parentRef);
|
|
154
151
|
}
|
package/dist/utils/elementRef.js
CHANGED
|
@@ -3,11 +3,17 @@ import { ast } from '../ast';
|
|
|
3
3
|
* Returns referenced AST Element
|
|
4
4
|
*/
|
|
5
5
|
export function elementRef(node) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
el
|
|
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.
|
|
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
|
-
|
|
20
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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?:
|
|
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 {
|
|
6
|
+
import { isOperationCancelled, OperationCancelled, URI, WorkspaceCache, } from 'langium';
|
|
6
7
|
import picomatch from 'picomatch';
|
|
7
|
-
import { filter, hasAtLeast, isNullish, map, pipe, prop,
|
|
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 ??
|
|
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 ===
|
|
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 ??
|
|
107
|
-
|
|
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
|
-
|
|
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 ===
|
|
174
|
-
return this.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
|
-
|
|
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
|
|
213
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
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
|
-
|
|
262
|
-
|
|
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],
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
.
|
|
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
|
-
|
|
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;
|