@likec4/language-server 1.32.1 → 1.32.2

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.
@@ -2,6 +2,8 @@ import type { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import type { LikeC4Services } from '../module';
4
4
  export interface LikeC4MCPServer {
5
+ readonly isStarted: boolean;
6
+ readonly port: number;
5
7
  start(port: number): Promise<void>;
6
8
  stop(): Promise<void>;
7
9
  }
@@ -9,6 +11,8 @@ export interface LikeC4MCPServerFactory {
9
11
  create(options?: ServerOptions): McpServer;
10
12
  }
11
13
  export declare class NoopLikeC4MCPServer implements LikeC4MCPServer {
14
+ get isStarted(): boolean;
15
+ get port(): number;
12
16
  start(port: number): Promise<never>;
13
17
  stop(): Promise<never>;
14
18
  }
@@ -1,4 +1,10 @@
1
1
  export class NoopLikeC4MCPServer {
2
+ get isStarted() {
3
+ return false;
4
+ }
5
+ get port() {
6
+ return NaN;
7
+ }
2
8
  start(port) {
3
9
  return Promise.reject(new Error("Not implemented"));
4
10
  }
@@ -3,10 +3,12 @@ import type { LikeC4Services } from '../../module';
3
3
  import type { LikeC4MCPServer } from '../LikeC4MCPServerFactory';
4
4
  export declare class SSELikeC4MCPServer implements LikeC4MCPServer, AsyncDisposable {
5
5
  private services;
6
- private readonly transports;
6
+ private transports;
7
7
  private server;
8
- private port;
8
+ private _port;
9
9
  constructor(services: LikeC4Services);
10
+ get isStarted(): boolean;
11
+ get port(): number;
10
12
  dispose(): Promise<void>;
11
13
  start(port?: number): Promise<void>;
12
14
  stop(): Promise<void>;
@@ -9,7 +9,13 @@ export class SSELikeC4MCPServer {
9
9
  // Store transports by session ID to send notifications
10
10
  transports = {};
11
11
  server = void 0;
12
- port = 33335;
12
+ _port = 33335;
13
+ get isStarted() {
14
+ return this.server?.listening === true;
15
+ }
16
+ get port() {
17
+ return this._port;
18
+ }
13
19
  async dispose() {
14
20
  await this.stop();
15
21
  }
@@ -20,8 +26,8 @@ export class SSELikeC4MCPServer {
20
26
  }
21
27
  await this.stop();
22
28
  }
23
- logger.info("Starting server on port {port}", { port });
24
- this.port = port;
29
+ logger.info("Starting MCP server on port {port}", { port });
30
+ this._port = port;
25
31
  const mcp = this.services.mcp.ServerFactory.create();
26
32
  const app = express();
27
33
  app.get("/sse", async (_, res) => {
@@ -44,13 +50,12 @@ export class SSELikeC4MCPServer {
44
50
  }
45
51
  });
46
52
  return new Promise((resolve, reject) => {
47
- this.server = app.listen(port, (err) => {
53
+ this.server = app.listen(this._port, (err) => {
48
54
  if (err) {
49
- logger.error("Failed to start server", { err });
50
55
  reject(err);
51
56
  return;
52
57
  }
53
- logger.info("server listening on port {port}", { port });
58
+ logger.info("MCP server listening on port {port}", { port: this._port });
54
59
  resolve();
55
60
  });
56
61
  });
@@ -62,6 +67,7 @@ export class SSELikeC4MCPServer {
62
67
  }
63
68
  logger.info("Stopping server");
64
69
  this.server = void 0;
70
+ this.transports = {};
65
71
  return new Promise((resolve) => {
66
72
  server.close((err) => {
67
73
  if (err) {
@@ -1,3 +1,5 @@
1
+ import { loggable } from "@likec4/log";
2
+ import { isError } from "remeda";
1
3
  import { logger } from "../../logger.js";
2
4
  import { SSELikeC4MCPServer } from "./MCPServer.js";
3
5
  import { LikeC4MCPServerFactory } from "./MCPServerFactory.js";
@@ -14,19 +16,33 @@ export const WithMCPServer = {
14
16
  return;
15
17
  }
16
18
  logger.debug("Configuration update: {update}", { update });
17
- const { enabled, port } = update.configuration.mcp ?? {
18
- enabled: false,
19
- port: 33335
20
- };
19
+ const {
20
+ enabled = false,
21
+ port = 33335
22
+ } = update.configuration.mcp;
21
23
  if (!enabled) {
22
24
  server.stop();
23
25
  return;
24
26
  }
25
27
  Promise.resolve().then(() => server.start(port)).then(() => {
26
28
  connection?.telemetry?.logEvent({
27
- eventName: "mcp-server-started"
29
+ eventName: "mcp-server-started",
30
+ mcpPort: port
28
31
  });
29
- }).catch((err) => logger.error("Failed to start LikeC4 MCP Server", { err }));
32
+ }).catch((err) => {
33
+ const message = isError(err) ? err.message : void 0;
34
+ connection?.telemetry?.logEvent({
35
+ eventName: "mcp-server-start-failed",
36
+ mcpPort: port,
37
+ ...message && { message }
38
+ });
39
+ logger.error("Failed to start LikeC4 MCP Server", { err });
40
+ if (connection) {
41
+ connection.window.showErrorMessage(`LikeC4: Failed to start MCP Server
42
+
43
+ ${loggable(err)}`);
44
+ }
45
+ });
30
46
  });
31
47
  return server;
32
48
  },
@@ -11,7 +11,6 @@ import {
11
11
  DocumentState,
12
12
  interruptAndCheck
13
13
  } from "langium";
14
- import prettyMs from "pretty-ms";
15
14
  import {
16
15
  filter,
17
16
  flatMap,
@@ -26,13 +25,13 @@ import {
26
25
  import { CancellationToken } from "vscode-jsonrpc";
27
26
  import { isLikeC4Builtin } from "../likec4lib.js";
28
27
  import { logger as mainLogger, logWarnError } from "../logger.js";
29
- import { ADisposable } from "../utils/index.js";
28
+ import { ADisposable, performanceMark } from "../utils/index.js";
30
29
  import { assignNavigateTo } from "../view-utils/index.js";
31
30
  import { buildModelData } from "./builder/buildModel.js";
32
31
  const parsedWithoutImportsCacheKey = (projectId) => `parsed-without-imports-${projectId}`;
33
32
  const parsedModelCacheKey = (projectId) => `parsed-model-${projectId}`;
34
33
  const computedModelCacheKey = (projectId) => `computed-model-${projectId}`;
35
- const logger = mainLogger.getChild("ModelBuilder");
34
+ const builderLogger = mainLogger.getChild("model-builder");
36
35
  export class DefaultLikeC4ModelBuilder extends ADisposable {
37
36
  projects;
38
37
  parser;
@@ -65,7 +64,7 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
65
64
  }
66
65
  )
67
66
  );
68
- logger.debug`created`;
67
+ builderLogger.debug`created`;
69
68
  }
70
69
  /**
71
70
  * WARNING:
@@ -76,19 +75,19 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
76
75
  */
77
76
  unsafeSyncParseModelData(projectId) {
78
77
  const cache = this.cache;
79
- const log = logger.getChild(["project", projectId]);
78
+ const logger = builderLogger.getChild(projectId);
80
79
  const key = parsedWithoutImportsCacheKey(projectId);
81
80
  if (cache.has(key)) {
82
- log.debug`unsafeSyncBuildModelData from cache, project ${projectId}`;
81
+ logger.debug`unsafeSyncBuildModelData from cache`;
83
82
  }
84
83
  return cache.get(key, () => {
85
84
  try {
86
85
  const docs = this.documents(projectId);
87
86
  if (docs.length === 0) {
88
- logger.debug`no documents to build model, project ${projectId}`;
87
+ logger.debug`no documents to build model`;
89
88
  return null;
90
89
  }
91
- log.debug`unsafeSyncBuildModelData, project ${projectId}`;
90
+ logger.debug`unsafeSyncBuildModelData`;
92
91
  return buildModelData(projectId, docs);
93
92
  } catch (e) {
94
93
  logWarnError(e);
@@ -100,6 +99,7 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
100
99
  * To avoid circular dependencies, first we parse all documents and then we join them.
101
100
  */
102
101
  unsafeSyncJoinedModelData(projectId) {
102
+ const logger = builderLogger.getChild(projectId);
103
103
  const cache = this.cache;
104
104
  const key = parsedModelCacheKey(projectId);
105
105
  return cache.get(key, () => {
@@ -133,18 +133,18 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
133
133
  }
134
134
  async parseModel(projectId, cancelToken = CancellationToken.None) {
135
135
  const project = this.projects.ensureProjectId(projectId);
136
- const log = logger.getChild(["project", project]);
136
+ const logger = builderLogger.getChild(project);
137
137
  const cache = this.cache;
138
138
  const cached = cache.get(parsedModelCacheKey(project));
139
139
  if (cached) {
140
- log.debug`parseModel from cache, project ${project}`;
140
+ logger.debug`parseModel from cache`;
141
141
  return cached;
142
142
  }
143
- const t0 = performance.now();
143
+ const t0 = performanceMark();
144
144
  return await this.mutex.read(async () => {
145
145
  await interruptAndCheck(cancelToken);
146
146
  const result = this.unsafeSyncJoinedModelData(project);
147
- log.debug(`parseModel, project ${project} in ${prettyMs(performance.now() - t0)}`);
147
+ logger.debug`parseModel in ${t0.pretty}`;
148
148
  return result;
149
149
  });
150
150
  }
@@ -155,6 +155,7 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
155
155
  * Otherwise, the model may be incomplete.
156
156
  */
157
157
  unsafeSyncBuildModel(projectId) {
158
+ const logger = builderLogger.getChild(projectId);
158
159
  const cache = this.cache;
159
160
  const viewsCache = this.cache;
160
161
  return cache.get(computedModelCacheKey(projectId), () => {
@@ -189,24 +190,24 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
189
190
  }
190
191
  async buildLikeC4Model(projectId, cancelToken = CancellationToken.None) {
191
192
  const project = this.projects.ensureProjectId(projectId);
192
- const log = logger.getChild(["project", project]);
193
+ const logger = builderLogger.getChild(project);
193
194
  const cache = this.cache;
194
195
  const cached = cache.get(computedModelCacheKey(project));
195
196
  if (cached) {
196
- log.debug("buildLikeC4Model from cache");
197
+ logger.debug("buildLikeC4Model from cache");
197
198
  return cached;
198
199
  }
199
- const t0 = performance.now();
200
+ const t0 = performanceMark();
200
201
  return await this.mutex.read(async () => {
201
202
  await interruptAndCheck(cancelToken);
202
203
  const result = this.unsafeSyncBuildModel(project);
203
- log.debug(`buildLikeC4Model in ${prettyMs(performance.now() - t0)}`);
204
+ logger.debug(`buildLikeC4Model in ${t0.pretty}`);
204
205
  return result;
205
206
  });
206
207
  }
207
208
  async computeView(viewId, projectId, cancelToken = CancellationToken.None) {
208
209
  const project = this.projects.ensureProjectId(projectId);
209
- const log = logger.getChild(["project", project]);
210
+ const logger = builderLogger.getChild(project);
210
211
  const cache = this.cache;
211
212
  const cacheKey = computedViewKey(project, viewId);
212
213
  if (cache.has(cacheKey)) {
@@ -219,10 +220,10 @@ export class DefaultLikeC4ModelBuilder extends ADisposable {
219
220
  return cache.get(cacheKey, () => {
220
221
  const view = parsed.$data.views[viewId];
221
222
  if (!view) {
222
- log.warn`computeView: cant find view ${viewId}`;
223
+ logger.warn`computeView: cant find view ${viewId}`;
223
224
  return null;
224
225
  }
225
- log.debug`computeView: ${viewId}`;
226
+ logger.debug`computeView: ${viewId}`;
226
227
  const result = computeView(view, parsed);
227
228
  if (!result.isSuccess) {
228
229
  logWarnError(result.error);
@@ -164,9 +164,11 @@ export function DeploymentModelParser(B) {
164
164
  const links = this.convertLinks(astNode.body);
165
165
  const kind = (astNode.kind ?? astNode.dotKind?.kind)?.ref?.name;
166
166
  const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty));
167
- const bodyProps = mapToObj(
168
- astNode.body?.props.filter(ast.isRelationStringProperty) ?? [],
169
- (p) => [p.key, p.value]
167
+ const bodyProps = pipe(
168
+ astNode.body?.props ?? [],
169
+ filter(ast.isRelationStringProperty),
170
+ filter((p) => isTruthy(p.value)),
171
+ mapToObj((p) => [p.key, p.value || void 0])
170
172
  );
171
173
  const navigateTo = pipe(
172
174
  astNode.body?.props ?? [],
@@ -175,8 +177,8 @@ export function DeploymentModelParser(B) {
175
177
  filter(isTruthy),
176
178
  first()
177
179
  );
178
- const title = removeIndent(astNode.title ?? bodyProps.title);
179
- const description = removeIndent(bodyProps.description);
180
+ const title = removeIndent(astNode.title ?? bodyProps.title) ?? "";
181
+ const description = removeIndent(astNode.description ?? bodyProps.description);
180
182
  const technology = toSingleLine(astNode.technology) ?? removeIndent(bodyProps.technology);
181
183
  const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty);
182
184
  const id = stringHash(
@@ -1,7 +1,7 @@
1
1
  import { invariant, isNonEmptyArray, LinkedList, nonexhaustive, nonNullable } from "@likec4/core";
2
2
  import { FqnRef } from "@likec4/core/types";
3
3
  import { loggable } from "@likec4/log";
4
- import { filter, first, isDefined, isEmpty, isNonNullish, isTruthy, map, mapToObj, pipe } from "remeda";
4
+ import { filter, first, isDefined, isEmpty, isTruthy, map, mapToObj, pipe } from "remeda";
5
5
  import {
6
6
  ast,
7
7
  toRelationshipStyleExcludeDefaults
@@ -140,9 +140,11 @@ ${error}`, {
140
140
  const kind = (astNode.kind ?? astNode.dotKind?.kind)?.ref?.name;
141
141
  const metadata = this.getMetadata(astNode.body?.props.find(ast.isMetadataProperty));
142
142
  const astPath = this.getAstNodePath(astNode);
143
- const bodyProps = mapToObj(
144
- astNode.body?.props.filter(ast.isRelationStringProperty).filter((p) => isNonNullish(p.value)) ?? [],
145
- (p) => [p.key, p.value]
143
+ const bodyProps = pipe(
144
+ astNode.body?.props ?? [],
145
+ filter(ast.isRelationStringProperty),
146
+ filter((p) => isTruthy(p.value)),
147
+ mapToObj((p) => [p.key, p.value || void 0])
146
148
  );
147
149
  const navigateTo = pipe(
148
150
  astNode.body?.props ?? [],
@@ -152,7 +154,7 @@ ${error}`, {
152
154
  first()
153
155
  );
154
156
  const title = removeIndent(astNode.title ?? bodyProps.title) ?? "";
155
- const description = removeIndent(bodyProps.description);
157
+ const description = removeIndent(astNode.description ?? bodyProps.description);
156
158
  const technology = toSingleLine(astNode.technology) ?? removeIndent(bodyProps.technology);
157
159
  const styleProp = astNode.body?.props.find(ast.isRelationStyleProperty);
158
160
  const id = stringHash(
package/dist/module.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphvizLayouter } from '@likec4/layouts';
1
+ import { QueueGraphvizLayoter } from '@likec4/layouts';
2
2
  import { type Module, WorkspaceCache } from 'langium';
3
3
  import { type DefaultSharedModuleContext, type LangiumServices, type LangiumSharedServices, type PartialLangiumServices } from 'langium/lsp';
4
4
  import { LikeC4DocumentationProvider } from './documentation';
@@ -43,7 +43,7 @@ export interface LikeC4AddedServices {
43
43
  likec4: {
44
44
  LanguageServices: LikeC4LanguageServices;
45
45
  Views: LikeC4Views;
46
- Layouter: GraphvizLayouter;
46
+ Layouter: QueueGraphvizLayoter;
47
47
  DeploymentsIndex: DeploymentsIndex;
48
48
  FqnIndex: FqnIndex;
49
49
  ModelParser: LikeC4ModelParser;
package/dist/module.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphvizLayouter, GraphvizWasmAdapter } from "@likec4/layouts";
1
+ import { GraphvizWasmAdapter, QueueGraphvizLayoter } from "@likec4/layouts";
2
2
  import {
3
3
  DocumentState,
4
4
  EmptyFileSystem,
@@ -16,7 +16,6 @@ import {
16
16
  LikeC4GeneratedSharedModule
17
17
  } from "./generated/module.js";
18
18
  import { DefaultLikeC4LanguageServices } from "./LikeC4LanguageServices.js";
19
- import { logger } from "./logger.js";
20
19
  import {
21
20
  LikeC4CodeLensProvider,
22
21
  LikeC4CompletionProvider,
@@ -87,8 +86,9 @@ export const LikeC4Module = {
87
86
  likec4: {
88
87
  LanguageServices: bind(DefaultLikeC4LanguageServices),
89
88
  Layouter: (_services) => {
90
- logger.debug("Creating GraphvizLayouter with GraphvizWasmAdapter");
91
- return new GraphvizLayouter(new GraphvizWasmAdapter());
89
+ return new QueueGraphvizLayoter({
90
+ graphviz: new GraphvizWasmAdapter()
91
+ });
92
92
  },
93
93
  Views: bind(DefaultLikeC4Views),
94
94
  DeploymentsIndex: bind(DeploymentsIndex),
@@ -4,3 +4,8 @@ export * from './fqnRef';
4
4
  export * from './projectId';
5
5
  export * from './stringHash';
6
6
  export declare function safeCall<T>(fn: () => T): T | undefined;
7
+ export declare function performanceNow(): number;
8
+ export declare function performanceMark(): {
9
+ readonly ms: number;
10
+ readonly pretty: string;
11
+ };
@@ -1,3 +1,4 @@
1
+ import prettyMs from "pretty-ms";
1
2
  import { logger } from "../logger.js";
2
3
  export * from "./disposable.js";
3
4
  export * from "./elementRef.js";
@@ -12,3 +13,21 @@ export function safeCall(fn) {
12
13
  return void 0;
13
14
  }
14
15
  }
16
+ export function performanceNow() {
17
+ try {
18
+ return performance.now();
19
+ } catch (e) {
20
+ return Date.now();
21
+ }
22
+ }
23
+ export function performanceMark() {
24
+ const t0 = performanceNow();
25
+ return {
26
+ get ms() {
27
+ return performanceNow() - t0;
28
+ },
29
+ get pretty() {
30
+ return prettyMs(performanceNow() - t0);
31
+ }
32
+ };
33
+ }
@@ -1,7 +1,7 @@
1
- import { GraphvizLayouter } from '@likec4/layouts';
1
+ import { QueueGraphvizLayoter } from '@likec4/layouts';
2
2
  import type { LikeC4Services } from '../module';
3
3
  export declare const ConfigurableLayouter: {
4
4
  likec4: {
5
- Layouter(services: LikeC4Services): GraphvizLayouter;
5
+ Layouter(services: LikeC4Services): QueueGraphvizLayoter;
6
6
  };
7
7
  };
@@ -1,4 +1,4 @@
1
- import { GraphvizLayouter, GraphvizWasmAdapter } from "@likec4/layouts";
1
+ import { GraphvizWasmAdapter, QueueGraphvizLayoter } from "@likec4/layouts";
2
2
  import { GraphvizBinaryAdapter } from "@likec4/layouts/graphviz/binary";
3
3
  import { isEmpty } from "remeda";
4
4
  import which from "which";
@@ -16,7 +16,9 @@ export const ConfigurableLayouter = {
16
16
  Layouter(services) {
17
17
  logger.debug("Creating ConfigurableLayouter");
18
18
  const wasmAdapter = new GraphvizWasmAdapter();
19
- const layouter = new GraphvizLayouter(wasmAdapter);
19
+ const layouter = new QueueGraphvizLayoter({
20
+ graphviz: wasmAdapter
21
+ });
20
22
  const langId = services.LanguageMetaData.languageId;
21
23
  services.shared.workspace.ConfigurationProvider.onConfigurationSectionUpdate((update) => {
22
24
  logger.debug("Configuration update: {update}", { update });
@@ -1,5 +1,5 @@
1
1
  import type { ComputedView, DiagramView, ProjectId, ViewId } from '@likec4/core';
2
- import { GraphvizLayouter } from '@likec4/layouts';
2
+ import { type QueueGraphvizLayoter, GraphvizLayouter } from '@likec4/layouts';
3
3
  import { CancellationToken } from 'vscode-jsonrpc';
4
4
  import type { LikeC4Services } from '../module';
5
5
  export type GraphvizOut = {
@@ -24,9 +24,8 @@ export declare class DefaultLikeC4Views implements LikeC4Views {
24
24
  private cache;
25
25
  private viewsWithReportedErrors;
26
26
  private ModelBuilder;
27
- private queue;
28
27
  constructor(services: LikeC4Services);
29
- get layouter(): GraphvizLayouter;
28
+ get layouter(): QueueGraphvizLayoter;
30
29
  computedViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<ComputedView[]>;
31
30
  layoutAllViews(projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<Array<Readonly<GraphvizOut>>>;
32
31
  layoutView(viewId: ViewId, projectId?: ProjectId | undefined, cancelToken?: CancellationToken): Promise<GraphvizOut | null>;
@@ -1,9 +1,9 @@
1
1
  import { loggable } from "@likec4/log";
2
- import PQueue from "p-queue";
3
- import prettyMs from "pretty-ms";
4
2
  import { values } from "remeda";
5
3
  import { CancellationToken } from "vscode-jsonrpc";
6
4
  import { logError, logger as rootLogger, logWarnError } from "../logger.js";
5
+ import { performanceMark } from "../utils/index.js";
6
+ const viewsLogger = rootLogger.getChild("views");
7
7
  export class DefaultLikeC4Views {
8
8
  constructor(services) {
9
9
  this.services = services;
@@ -12,7 +12,6 @@ export class DefaultLikeC4Views {
12
12
  cache = /* @__PURE__ */ new WeakMap();
13
13
  viewsWithReportedErrors = /* @__PURE__ */ new Set();
14
14
  ModelBuilder;
15
- queue = new PQueue({ concurrency: 2, timeout: 2e4, throwOnTimeout: true });
16
15
  get layouter() {
17
16
  return this.services.likec4.Layouter;
18
17
  }
@@ -26,16 +25,10 @@ export class DefaultLikeC4Views {
26
25
  if (views.length === 0) {
27
26
  return [];
28
27
  }
29
- const logger = rootLogger.getChild(["views", projectId ?? ""]);
28
+ const m0 = performanceMark();
29
+ const logger = projectId ? viewsLogger.getChild(projectId) : viewsLogger;
30
30
  logger.debug`layoutAll: ${views.length} views`;
31
- if (this.queue.pending + this.queue.size > 0) {
32
- logger.debug`wait for previous layouts to finish`;
33
- await this.queue.onIdle();
34
- }
35
- if (this.layouter.port.concurrency !== this.queue.concurrency) {
36
- this.queue.concurrency = this.layouter.port.concurrency;
37
- logger.debug`set queue concurrency to ${this.layouter.port.concurrency}`;
38
- }
31
+ const tasks = [];
39
32
  const specification = likeC4Model.$data.specification;
40
33
  const results = [];
41
34
  for (const view of views) {
@@ -45,40 +38,36 @@ export class DefaultLikeC4Views {
45
38
  results.push(cached);
46
39
  continue;
47
40
  }
48
- if (this.queue.pending > this.queue.concurrency + 4) {
49
- await this.queue.onSizeLessThan(this.queue.concurrency + 1);
50
- }
51
- this.queue.add(async () => {
52
- logger.debug`layouting view ${view.id}...`;
53
- return await this.layouter.layout({
54
- view,
55
- specification
56
- });
57
- }).then((result) => {
58
- if (!result) {
59
- throw new Error(`Layout queue returned null for view ${view.id}`);
41
+ tasks.push({
42
+ view,
43
+ specification
44
+ });
45
+ }
46
+ if (tasks.length > 0) {
47
+ await this.layouter.batchLayout({
48
+ batch: tasks,
49
+ onSuccess: (task, result) => {
50
+ this.viewsWithReportedErrors.delete(task.view.id);
51
+ this.cache.set(task.view, result);
52
+ results.push(result);
53
+ },
54
+ onError: (task, error) => {
55
+ logger.warn(`Fail layout view ${task.view.id}`, { error });
56
+ this.cache.delete(task.view);
60
57
  }
61
- this.viewsWithReportedErrors.delete(view.id);
62
- logger.debug`done layout view ${view.id}`;
63
- this.cache.set(view, result);
64
- results.push(result);
65
- }).catch((e) => {
66
- logger.error(`Fail layout view ${view.id}`, { e });
67
- this.cache.delete(view);
68
58
  });
69
59
  }
70
- await this.queue.onIdle();
71
60
  if (results.length !== views.length) {
72
- logger.warn`layouted ${results.length} of ${views.length} views`;
61
+ logger.warn`layouted ${results.length} of ${views.length} views in ${m0.pretty}`;
73
62
  } else if (results.length > 0) {
74
- logger.debug`layouted all ${results.length} views`;
63
+ logger.debug`layouted all ${results.length} views in ${m0.pretty}`;
75
64
  }
76
65
  return results;
77
66
  }
78
67
  async layoutView(viewId, projectId, cancelToken = CancellationToken.None) {
79
68
  const model = await this.ModelBuilder.buildLikeC4Model(projectId, cancelToken);
80
69
  const view = model.findView(viewId)?.$view;
81
- const logger = rootLogger.getChild(["views", projectId ?? ""]);
70
+ const logger = projectId ? viewsLogger.getChild(projectId) : viewsLogger;
82
71
  if (!view) {
83
72
  logger.warn`layoutView ${viewId} not found`;
84
73
  return null;
@@ -89,24 +78,14 @@ export class DefaultLikeC4Views {
89
78
  return await Promise.resolve(cached);
90
79
  }
91
80
  try {
92
- const start = performance.now();
93
- if (this.queue.pending + this.queue.size > 0) {
94
- logger.debug`wait for previous layouts to finish`;
95
- await this.queue.onIdle();
96
- }
97
- const result = await this.queue.add(async () => {
98
- logger.debug`layouting view ${view.id}...`;
99
- return await this.layouter.layout({
100
- view,
101
- specification: model.$data.specification
102
- });
81
+ const m0 = performanceMark();
82
+ const result = await this.layouter.layout({
83
+ view,
84
+ specification: model.$data.specification
103
85
  });
104
- if (!result) {
105
- throw new Error(`Failed to layout view ${viewId}`);
106
- }
107
86
  this.viewsWithReportedErrors.delete(viewId);
108
87
  this.cache.set(view, result);
109
- logger.debug(`layout {viewId} ready in ${prettyMs(performance.now() - start)}`, { viewId });
88
+ logger.debug(`layout {viewId} ready in ${m0.pretty}`, { viewId });
110
89
  return result;
111
90
  } catch (e) {
112
91
  if (!this.viewsWithReportedErrors.has(viewId)) {
@@ -50,7 +50,6 @@ export declare class ProjectsManager {
50
50
  * Checks if the provided file system entry is a valid project config file.
51
51
  *
52
52
  * @param entry The file system entry to check
53
- * @returns {boolean} Returns true if the entry is a valid config file, false otherwise.
54
53
  */
55
54
  loadConfigFile(entry: FileSystemNode): Promise<Project | undefined>;
56
55
  registerProject(configFile: URI): Promise<Project>;
@@ -1,4 +1,5 @@
1
1
  import { BiMap, invariant, nonNullable } from "@likec4/core";
2
+ import { loggable } from "@likec4/log";
2
3
  import { URI, WorkspaceCache } from "langium";
3
4
  import picomatch from "picomatch/posix";
4
5
  import { hasAtLeast, isNullish, map, pipe, prop, sortBy } from "remeda";
@@ -9,7 +10,7 @@ import {
9
10
  withoutProtocol,
10
11
  withProtocol
11
12
  } from "ufo";
12
- import { parseConfigJson } from "../config/index.js";
13
+ import { parseConfigJson, validateConfig } from "../config/index.js";
13
14
  import { logger as mainLogger } from "../logger.js";
14
15
  const logger = mainLogger.getChild("ProjectsManager");
15
16
  export class ProjectsManager {
@@ -115,14 +116,23 @@ export class ProjectsManager {
115
116
  * Checks if the provided file system entry is a valid project config file.
116
117
  *
117
118
  * @param entry The file system entry to check
118
- * @returns {boolean} Returns true if the entry is a valid config file, false otherwise.
119
119
  */
120
120
  async loadConfigFile(entry) {
121
121
  if (entry.isDirectory) {
122
122
  return void 0;
123
123
  }
124
124
  if (this.isConfigFile(entry)) {
125
- return await this.registerProject(entry.uri);
125
+ try {
126
+ return await this.registerProject(entry.uri);
127
+ } catch (error) {
128
+ this.services.lsp.Connection?.window.showErrorMessage(
129
+ `LikeC4: Failed to register project at ${entry.uri.toString()}
130
+
131
+ ${loggable(error)}`
132
+ );
133
+ logger.error("Failed to register project at {uri}", { uri: entry.uri.toString(), error });
134
+ return void 0;
135
+ }
126
136
  }
127
137
  return void 0;
128
138
  }
@@ -135,7 +145,8 @@ export class ProjectsManager {
135
145
  const folderUri2 = configFile.with({ path });
136
146
  return this.registerProject({ config: config2, folderUri: folderUri2 });
137
147
  }
138
- const { config, folderUri } = opts;
148
+ const config = validateConfig(opts.config);
149
+ const { folderUri } = opts;
139
150
  let id = config.name;
140
151
  let i = 1;
141
152
  while (this.projectIdToFolder.has(id)) {