@theia/dev-container 1.71.0-next.8 → 1.71.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 (56) hide show
  1. package/README.md +1 -0
  2. package/lib/electron-browser/container-connection-contribution.d.ts +8 -0
  3. package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
  4. package/lib/electron-browser/container-connection-contribution.js +158 -6
  5. package/lib/electron-browser/container-connection-contribution.js.map +1 -1
  6. package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
  7. package/lib/electron-browser/dev-container-frontend-module.js +3 -0
  8. package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
  9. package/lib/electron-browser/dev-container-suggestion-contribution.d.ts +16 -0
  10. package/lib/electron-browser/dev-container-suggestion-contribution.d.ts.map +1 -0
  11. package/lib/electron-browser/dev-container-suggestion-contribution.js +96 -0
  12. package/lib/electron-browser/dev-container-suggestion-contribution.js.map +1 -0
  13. package/lib/electron-common/remote-container-connection-provider.d.ts +9 -0
  14. package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
  15. package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
  16. package/lib/electron-node/dev-container-file-service.js +4 -6
  17. package/lib/electron-node/dev-container-file-service.js.map +1 -1
  18. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.d.ts.map +1 -1
  19. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.js +7 -1
  20. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.js.map +1 -1
  21. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.d.ts +2 -0
  22. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.d.ts.map +1 -0
  23. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.js +421 -0
  24. package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.js.map +1 -0
  25. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +28 -1
  26. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts.map +1 -1
  27. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js +304 -4
  28. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js.map +1 -1
  29. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts.map +1 -1
  30. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +0 -1
  31. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
  32. package/lib/electron-node/devcontainer-file.d.ts +8 -1
  33. package/lib/electron-node/devcontainer-file.d.ts.map +1 -1
  34. package/lib/electron-node/devcontainer-file.js +14 -0
  35. package/lib/electron-node/devcontainer-file.js.map +1 -1
  36. package/lib/electron-node/remote-container-connection-provider.d.ts +6 -1
  37. package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
  38. package/lib/electron-node/remote-container-connection-provider.js +112 -4
  39. package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
  40. package/lib/electron-node/remote-container-connection-provider.spec.d.ts +2 -0
  41. package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +1 -0
  42. package/lib/electron-node/remote-container-connection-provider.spec.js +131 -0
  43. package/lib/electron-node/remote-container-connection-provider.spec.js.map +1 -0
  44. package/package.json +10 -10
  45. package/src/electron-browser/container-connection-contribution.ts +173 -7
  46. package/src/electron-browser/dev-container-frontend-module.ts +4 -0
  47. package/src/electron-browser/dev-container-suggestion-contribution.ts +93 -0
  48. package/src/electron-common/remote-container-connection-provider.ts +10 -0
  49. package/src/electron-node/dev-container-file-service.ts +4 -6
  50. package/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.ts +519 -0
  51. package/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.ts +7 -1
  52. package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +323 -5
  53. package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +0 -1
  54. package/src/electron-node/devcontainer-file.ts +13 -1
  55. package/src/electron-node/remote-container-connection-provider.spec.ts +152 -0
  56. package/src/electron-node/remote-container-connection-provider.ts +121 -5
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { inject, injectable } from '@theia/core/shared/inversify';
17
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
18
18
  import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
19
19
  import { DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
20
20
  import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
@@ -32,9 +32,29 @@ export namespace RemoteContainerCommands {
32
32
  label: 'Reopen in Container',
33
33
  category: 'Dev Container'
34
34
  }, 'theia/remote/dev-container/connect');
35
+
36
+ export const ATTACH_TO_CONTAINER = Command.toLocalizedCommand({
37
+ id: 'dev-container:attach-to-container',
38
+ label: 'Attach to Running Container',
39
+ category: 'Dev Container'
40
+ }, 'theia/remote/dev-container/attach');
41
+
42
+ export const REBUILD_CONTAINER = Command.toLocalizedCommand({
43
+ id: 'dev-container:rebuild-container',
44
+ label: 'Rebuild Container',
45
+ category: 'Dev Container'
46
+ }, 'theia/remote/dev-container/rebuild');
35
47
  }
36
48
 
37
49
  const LAST_USED_CONTAINER = 'lastUsedContainer';
50
+ const ACTIVE_DEV_CONTAINER_CONTEXT = 'activeDevContainerContext';
51
+
52
+ interface DevContainerContext {
53
+ devcontainerFilePath: string;
54
+ devcontainerFileName: string;
55
+ hostWorkspacePath: string;
56
+ containerId: string;
57
+ }
38
58
  @injectable()
39
59
  export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution implements WorkspaceOpenHandlerContribution {
40
60
 
@@ -65,28 +85,85 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
65
85
  @inject(ContainerOutputProvider)
66
86
  protected readonly containerOutputProvider: ContainerOutputProvider;
67
87
 
88
+ protected hasDevContainerFiles = false;
89
+
90
+ @postConstruct()
91
+ protected init(): void {
92
+ // Mark that we're in a remote session. sessionStorage survives page
93
+ // reloads (disconnect) but is cleared on window close (restart).
94
+ // This lets canHandle() distinguish disconnect from restart.
95
+ if (this.isRemoteSession()) {
96
+ sessionStorage.setItem('devcontainer:wasRemote', 'true');
97
+ }
98
+ this.workspaceService.ready.then(() => this.checkForDevContainerFiles());
99
+ this.workspaceService.onWorkspaceChanged(() => this.checkForDevContainerFiles());
100
+ }
101
+
102
+ protected async checkForDevContainerFiles(): Promise<void> {
103
+ if (this.isRemoteSession()) {
104
+ this.hasDevContainerFiles = true;
105
+ return;
106
+ }
107
+ const workspace = this.workspaceService.workspace;
108
+ if (!workspace) {
109
+ this.hasDevContainerFiles = false;
110
+ return;
111
+ }
112
+ try {
113
+ const files = await this.connectionProvider.getDevContainerFiles(workspace.resource.path.toString());
114
+ this.hasDevContainerFiles = files.length > 0;
115
+ } catch (error) {
116
+ // Failed to check for devcontainer files, assume none exist
117
+ this.hasDevContainerFiles = false;
118
+ }
119
+ }
120
+
68
121
  registerRemoteCommands(registry: RemoteRegistry): void {
69
122
  registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
70
- execute: () => this.openInContainer()
123
+ execute: () => this.openInContainer(),
124
+ isVisible: () => !this.isRemoteSession() && this.hasDevContainerFiles
71
125
  });
126
+ registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
127
+ execute: () => this.attachToContainer()
128
+ });
129
+ registry.registerCommand(RemoteContainerCommands.REBUILD_CONTAINER, {
130
+ execute: () => this.rebuildContainer(),
131
+ isVisible: () => this.isRemoteSession()
132
+ });
133
+ }
134
+
135
+ protected isRemoteSession(): boolean {
136
+ return new URLSearchParams(window.location.search).has('localPort');
72
137
  }
73
138
 
74
139
  canHandle(uri: URI): MaybePromise<boolean> {
75
- return uri.scheme === DEV_CONTAINER_WORKSPACE_SCHEME;
140
+ if (uri.scheme !== DEV_CONTAINER_WORKSPACE_SCHEME) {
141
+ return false;
142
+ }
143
+ // After disconnect (reload), sessionStorage still has the flag from
144
+ // the remote session's init. Skip auto-reopen so the user gets their
145
+ // local workspace. After restart (close+open), sessionStorage is
146
+ // cleared, so auto-reopen works.
147
+ const wasRemote = sessionStorage.getItem('devcontainer:wasRemote');
148
+ if (wasRemote) {
149
+ sessionStorage.removeItem('devcontainer:wasRemote');
150
+ return false;
151
+ }
152
+ return true;
76
153
  }
77
154
 
78
155
  async openWorkspace(uri: URI, options?: WorkspaceInput | undefined): Promise<void> {
79
156
  const filePath = new URLSearchParams(uri.query).get(DEV_CONTAINER_PATH_QUERY);
80
157
 
81
158
  if (!filePath) {
82
- throw new Error('No devcontainer file specified for workspace');
159
+ throw new Error(nls.localize('theia/dev-container/noDevcontainerFileSpecified', 'No devcontainer file specified for workspace'));
83
160
  }
84
161
 
85
162
  const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(uri.path.toString());
86
163
  const devcontainerFile = devcontainerFiles.find(file => file.path === filePath);
87
164
 
88
165
  if (!devcontainerFile) {
89
- throw new Error(`Devcontainer file at ${filePath} not found in workspace`);
166
+ throw new Error(nls.localize('theia/dev-container/devcontainerFileNotFound', 'Devcontainer file at {0} not found in workspace', filePath));
90
167
  }
91
168
 
92
169
  return this.doOpenInContainer(devcontainerFile, uri.path.toString());
@@ -110,17 +187,98 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
110
187
  this.doOpenInContainer(devcontainerFile);
111
188
  }
112
189
 
190
+ async attachToContainer(): Promise<void> {
191
+ const containers = await this.connectionProvider.listRunningContainers();
192
+ if (containers.length === 0) {
193
+ this.messageService.info(nls.localize('theia/remote/dev-container/noRunningContainers', 'No running containers found.'));
194
+ return;
195
+ }
196
+
197
+ const selected = await this.quickInputService.pick(containers.map(container => ({
198
+ type: 'item' as const,
199
+ label: container.name || container.id.substring(0, 12),
200
+ description: container.image,
201
+ detail: container.status,
202
+ container
203
+ })), {
204
+ title: nls.localize('theia/remote/dev-container/selectContainer', 'Select a running container to attach to')
205
+ });
206
+
207
+ if (!selected) {
208
+ return;
209
+ }
210
+
211
+ this.containerOutputProvider.openChannel();
212
+
213
+ const connectionResult = await this.connectionProvider.attachToContainer(selected.container.id);
214
+ this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
215
+ }
216
+
217
+ async rebuildContainer(): Promise<void> {
218
+ this.containerOutputProvider.openChannel();
219
+ const progress = await this.messageService.showProgress({
220
+ text: nls.localize('theia/remote/dev-container/rebuilding', 'Rebuilding dev container')
221
+ });
222
+
223
+ try {
224
+ // When inside a remote container, read the stored context instead of
225
+ // scanning the filesystem (the RPC goes to the local backend which
226
+ // doesn't have the container's workspace path).
227
+ const ctx = await this.storageService.getData<DevContainerContext | undefined>(ACTIVE_DEV_CONTAINER_CONTEXT);
228
+ if (ctx) {
229
+ progress.report({ message: nls.localize('theia/dev-container/removingOldContainer', 'Removing old container...') });
230
+ try {
231
+ await this.connectionProvider.removeContainer(ctx.containerId);
232
+ } catch (error) {
233
+ // Container may already be gone, ignore error
234
+ }
235
+ const lastContainerKey = `${LAST_USED_CONTAINER}:${ctx.devcontainerFilePath}`;
236
+ await this.storageService.setData(lastContainerKey, undefined);
237
+ progress.cancel();
238
+ this.doOpenInContainer(
239
+ { path: ctx.devcontainerFilePath, name: ctx.devcontainerFileName },
240
+ ctx.hostWorkspacePath
241
+ );
242
+ return;
243
+ }
244
+
245
+ // Fallback: local workspace — scan for devcontainer files
246
+ const devcontainerFile = await this.getOrSelectDevcontainerFile();
247
+ if (!devcontainerFile) {
248
+ return;
249
+ }
250
+ const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
251
+ const lastContainerInfo = await this.storageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
252
+ if (lastContainerInfo) {
253
+ progress.report({ message: nls.localize('theia/dev-container/removingOldContainer', 'Removing old container...') });
254
+ try {
255
+ await this.connectionProvider.removeContainer(lastContainerInfo.id);
256
+ } catch (error) {
257
+ // Container may already be gone, ignore error
258
+ }
259
+ await this.storageService.setData(lastContainerInfoKey, undefined);
260
+ }
261
+ progress.cancel();
262
+ this.doOpenInContainer(devcontainerFile);
263
+ } catch (e) {
264
+ progress.cancel();
265
+ this.messageService.error(nls.localize('theia/dev-container/failedToRebuild', 'Failed to rebuild container: {0}', (e as Error).message));
266
+ }
267
+ }
268
+
113
269
  async doOpenInContainer(devcontainerFile: DevContainerFile, workspacePath?: string): Promise<void> {
114
270
  const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
115
271
  const lastContainerInfo = await this.storageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
116
272
 
117
273
  this.containerOutputProvider.openChannel();
118
274
 
275
+ const hostWorkspacePath = workspacePath ?? this.workspaceService.workspace?.resource.path.toString();
276
+
119
277
  const connectionResult = await this.connectionProvider.connectToContainer({
120
278
  nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
121
279
  lastContainerInfo,
122
280
  devcontainerFile: devcontainerFile.path,
123
- workspacePath: workspacePath
281
+ workspacePath: hostWorkspacePath
124
282
  });
125
283
 
126
284
  this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
@@ -128,8 +286,16 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
128
286
  lastUsed: Date.now()
129
287
  });
130
288
 
289
+ // Store full context so rebuild works from inside the container
290
+ this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
291
+ devcontainerFilePath: devcontainerFile.path,
292
+ devcontainerFileName: devcontainerFile.name,
293
+ hostWorkspacePath: hostWorkspacePath ?? '',
294
+ containerId: connectionResult.containerId,
295
+ });
296
+
131
297
  this.workspaceServer.setMostRecentlyUsedWorkspace(
132
- `${DEV_CONTAINER_WORKSPACE_SCHEME}:${workspacePath ?? this.workspaceService.workspace?.resource.path}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
298
+ `${DEV_CONTAINER_WORKSPACE_SCHEME}:${hostWorkspacePath}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
133
299
 
134
300
  this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
135
301
  }
@@ -23,6 +23,7 @@ import { ContainerInfoContribution } from './container-info-contribution';
23
23
  import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
24
24
  import { WorkspaceOpenHandlerContribution } from '@theia/workspace/lib/browser/workspace-service';
25
25
  import { WindowTitleContribution } from '@theia/core/lib/browser/window/window-title-service';
26
+ import { DevContainerSuggestionContribution } from './dev-container-suggestion-contribution';
26
27
 
27
28
  export default new ContainerModule(bind => {
28
29
  bind(ContainerConnectionContribution).toSelf().inSingletonScope();
@@ -40,4 +41,7 @@ export default new ContainerModule(bind => {
40
41
  bind(FrontendApplicationContribution).toService(ContainerInfoContribution);
41
42
  bind(WindowTitleContribution).toService(ContainerInfoContribution);
42
43
  bind(LabelProviderContribution).toService(ContainerInfoContribution);
44
+
45
+ bind(DevContainerSuggestionContribution).toSelf().inSingletonScope();
46
+ bind(FrontendApplicationContribution).toService(DevContainerSuggestionContribution);
43
47
  });
@@ -0,0 +1,93 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { CommandService, MessageService, nls } from '@theia/core';
19
+ import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
20
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
21
+ import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
22
+ import { RemoteContainerCommands } from './container-connection-contribution';
23
+ import { RemoteStatusService } from '@theia/remote/lib/electron-common/remote-status-service';
24
+
25
+ const DONT_SHOW_AGAIN_KEY = 'dev-container.suggestion.dontShowAgain';
26
+
27
+ @injectable()
28
+ export class DevContainerSuggestionContribution implements FrontendApplicationContribution {
29
+
30
+ @inject(WorkspaceService)
31
+ protected readonly workspaceService: WorkspaceService;
32
+
33
+ @inject(RemoteContainerConnectionProvider)
34
+ protected readonly connectionProvider: RemoteContainerConnectionProvider;
35
+
36
+ @inject(MessageService)
37
+ protected readonly messageService: MessageService;
38
+
39
+ @inject(CommandService)
40
+ protected readonly commandService: CommandService;
41
+
42
+ @inject(RemoteStatusService)
43
+ protected readonly remoteStatusService: RemoteStatusService;
44
+
45
+ @inject(LocalStorageService)
46
+ protected readonly storageService: LocalStorageService;
47
+
48
+ onStart(): void {
49
+ this.checkForDevContainer();
50
+ }
51
+
52
+ protected async checkForDevContainer(): Promise<void> {
53
+ const containerPort = parseInt(new URLSearchParams(location.search).get('port') ?? '0');
54
+ if (containerPort > 0) {
55
+ const status = await this.remoteStatusService.getStatus(containerPort);
56
+ if (status?.alive) {
57
+ return;
58
+ }
59
+ }
60
+
61
+ const dontShowAgain = await this.storageService.getData<boolean>(DONT_SHOW_AGAIN_KEY);
62
+ if (dontShowAgain) {
63
+ return;
64
+ }
65
+
66
+ await this.workspaceService.ready;
67
+ const workspace = this.workspaceService.workspace;
68
+ if (!workspace) {
69
+ return;
70
+ }
71
+
72
+ try {
73
+ const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(workspace.resource.path.toString());
74
+ if (devcontainerFiles.length > 0) {
75
+ const reopenAction = nls.localize('theia/remote/dev-container/reopenInContainer', 'Reopen in Container');
76
+ const dontShowAgainAction = nls.localizeByDefault("Don't Show Again");
77
+ const result = await this.messageService.info(
78
+ nls.localize('theia/remote/dev-container/suggestion',
79
+ 'This workspace has a dev container configuration. Would you like to reopen it in a container?'),
80
+ reopenAction,
81
+ dontShowAgainAction
82
+ );
83
+ if (result === reopenAction) {
84
+ this.commandService.executeCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER.id);
85
+ } else if (result === dontShowAgainAction) {
86
+ await this.storageService.setData(DONT_SHOW_AGAIN_KEY, true);
87
+ }
88
+ }
89
+ } catch (error) {
90
+ // Silently ignore if we can't check for devcontainer files
91
+ }
92
+ }
93
+ }
@@ -45,8 +45,18 @@ export interface DevContainerFile {
45
45
  path: string;
46
46
  }
47
47
 
48
+ export interface RunningContainerInfo {
49
+ id: string;
50
+ name: string;
51
+ image: string;
52
+ status: string;
53
+ }
54
+
48
55
  export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOutputProvider> {
49
56
  connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult>;
50
57
  getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]>;
51
58
  getCurrentContainerInfo(port: number): Promise<ContainerInspectInfo | undefined>;
59
+ listRunningContainers(): Promise<RunningContainerInfo[]>;
60
+ attachToContainer(containerId: string): Promise<ContainerConnectionResult>;
61
+ removeContainer(containerId: string): Promise<void>;
52
62
  }
@@ -23,7 +23,7 @@ import * as fs from '@theia/core/shared/fs-extra';
23
23
  import { ContributionProvider, Path, URI } from '@theia/core';
24
24
  import { VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
25
25
 
26
- const VARIABLE_REGEX = /^\$\{(.+?)(?::(.+))?\}$/;
26
+ const VARIABLE_REGEX = /\$\{(.+?)(?::(.+?))?\}/g;
27
27
 
28
28
  @injectable()
29
29
  export class DevContainerFileService {
@@ -35,16 +35,14 @@ export class DevContainerFileService {
35
35
  protected readonly variableResolverContributions: ContributionProvider<VariableResolverContribution>;
36
36
 
37
37
  protected resolveVariable(value: string): string {
38
- const match = value.match(VARIABLE_REGEX);
39
- if (match) {
40
- const [, type, variable] = match;
38
+ return value.replace(VARIABLE_REGEX, (match, type, variable) => {
41
39
  for (const contribution of this.variableResolverContributions.getContributions()) {
42
40
  if (contribution.canResolve(type)) {
43
41
  return contribution.resolve(variable ?? type);
44
42
  }
45
43
  }
46
- }
47
- return value;
44
+ return match;
45
+ });
48
46
  }
49
47
 
50
48
  protected resolveVariablesRecursively<T>(obj: T): T {