@theia/workspace 1.68.0 → 1.68.1

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 (34) hide show
  1. package/lib/browser/index.d.ts +1 -0
  2. package/lib/browser/index.d.ts.map +1 -1
  3. package/lib/browser/index.js +1 -0
  4. package/lib/browser/index.js.map +1 -1
  5. package/lib/browser/metadata-storage/index.d.ts +18 -0
  6. package/lib/browser/metadata-storage/index.d.ts.map +1 -0
  7. package/lib/browser/metadata-storage/index.js +25 -0
  8. package/lib/browser/metadata-storage/index.js.map +1 -0
  9. package/lib/browser/metadata-storage/workspace-metadata-storage-service.d.ts +94 -0
  10. package/lib/browser/metadata-storage/workspace-metadata-storage-service.d.ts.map +1 -0
  11. package/lib/browser/metadata-storage/workspace-metadata-storage-service.js +191 -0
  12. package/lib/browser/metadata-storage/workspace-metadata-storage-service.js.map +1 -0
  13. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.d.ts +17 -0
  14. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.d.ts.map +1 -0
  15. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js +279 -0
  16. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js.map +1 -0
  17. package/lib/browser/metadata-storage/workspace-metadata-store.d.ts +81 -0
  18. package/lib/browser/metadata-storage/workspace-metadata-store.d.ts.map +1 -0
  19. package/lib/browser/metadata-storage/workspace-metadata-store.js +137 -0
  20. package/lib/browser/metadata-storage/workspace-metadata-store.js.map +1 -0
  21. package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
  22. package/lib/browser/workspace-frontend-module.js +6 -0
  23. package/lib/browser/workspace-frontend-module.js.map +1 -1
  24. package/lib/browser/workspace-trust-service.d.ts.map +1 -1
  25. package/lib/browser/workspace-trust-service.js +4 -4
  26. package/lib/browser/workspace-trust-service.js.map +1 -1
  27. package/package.json +6 -6
  28. package/src/browser/index.ts +1 -0
  29. package/src/browser/metadata-storage/index.ts +23 -0
  30. package/src/browser/metadata-storage/workspace-metadata-storage-service.spec.ts +342 -0
  31. package/src/browser/metadata-storage/workspace-metadata-storage-service.ts +244 -0
  32. package/src/browser/metadata-storage/workspace-metadata-store.ts +172 -0
  33. package/src/browser/workspace-frontend-module.ts +7 -0
  34. package/src/browser/workspace-trust-service.ts +4 -5
@@ -0,0 +1,244 @@
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, named } from '@theia/core/shared/inversify';
18
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
19
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
20
+ import { ILogger } from '@theia/core/lib/common/logger';
21
+ import { URI } from '@theia/core/lib/common/uri';
22
+ import { generateUuid } from '@theia/core/lib/common/uuid';
23
+ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
24
+ import { WorkspaceService } from '../workspace-service';
25
+ import { WorkspaceMetadataStore, WorkspaceMetadataStoreImpl } from './workspace-metadata-store';
26
+
27
+ export const WorkspaceMetadataStoreFactory = Symbol('WorkspaceMetadataStoreFactory');
28
+ export type WorkspaceMetadataStoreFactory = () => WorkspaceMetadataStoreImpl;
29
+
30
+ /**
31
+ * Index mapping workspace root paths to UUIDs.
32
+ * Stored at $CONFIGDIR/workspace-metadata/index.json
33
+ */
34
+ export interface WorkspaceMetadataIndex {
35
+ [workspacePath: string]: string; // workspace path -> UUID
36
+ }
37
+
38
+ /**
39
+ * Service for managing workspace-specific metadata storage.
40
+ * Provides isolated storage directories for different features within a workspace.
41
+ *
42
+ * This is different to the `WorkspaceStorageService` in that it is an unlimited free-form
43
+ * storage area _in the filesystem_ and not in the browser's local storage.
44
+ */
45
+ export const WorkspaceMetadataStorageService = Symbol('WorkspaceMetadataStorageService');
46
+ export interface WorkspaceMetadataStorageService {
47
+ /**
48
+ * Gets an existing metadata store for the given key, or creates a new one if it doesn't exist.
49
+ *
50
+ * @param key A unique identifier for the metadata store. Special characters will be replaced with hyphens.
51
+ * @returns The existing or newly created WorkspaceMetadataStore instance
52
+ * @throws Error if no workspace is currently open
53
+ */
54
+ getOrCreateStore(key: string): Promise<WorkspaceMetadataStore>;
55
+ }
56
+
57
+ @injectable()
58
+ export class WorkspaceMetadataStorageServiceImpl implements WorkspaceMetadataStorageService {
59
+
60
+ @inject(FileService)
61
+ protected readonly fileService: FileService;
62
+
63
+ @inject(WorkspaceService)
64
+ protected readonly workspaceService: WorkspaceService;
65
+
66
+ @inject(EnvVariablesServer)
67
+ protected readonly envVariableServer: EnvVariablesServer;
68
+
69
+ @inject(ILogger) @named('WorkspaceMetadataStorage')
70
+ protected readonly logger: ILogger;
71
+
72
+ @inject(WorkspaceMetadataStoreFactory)
73
+ protected readonly storeFactory: WorkspaceMetadataStoreFactory;
74
+
75
+ /**
76
+ * Registry of created stores by their mangled keys
77
+ */
78
+ protected readonly stores = new Map<string, WorkspaceMetadataStore>();
79
+
80
+ /**
81
+ * Cached metadata root directory (e.g., file://$CONFIGDIR/workspace-metadata/)
82
+ */
83
+ protected metadataRoot?: URI;
84
+
85
+ /**
86
+ * Cached index file location
87
+ */
88
+ protected indexFile?: URI;
89
+
90
+ async getOrCreateStore(key: string): Promise<WorkspaceMetadataStore> {
91
+ const mangledKey = this.mangleKey(key);
92
+
93
+ const existingStore = this.stores.get(mangledKey);
94
+ if (existingStore) {
95
+ this.logger.debug(`Returning existing metadata store for key '${key}'`, {
96
+ mangledKey,
97
+ location: existingStore.location.toString()
98
+ });
99
+ return existingStore;
100
+ }
101
+
102
+ return this.doCreateStore(key, mangledKey);
103
+ }
104
+
105
+ protected async doCreateStore(key: string, mangledKey: string): Promise<WorkspaceMetadataStore> {
106
+ const workspaceRoot = this.getFirstWorkspaceRoot();
107
+ if (!workspaceRoot) {
108
+ throw new Error('Cannot create metadata store: no workspace is currently open');
109
+ }
110
+
111
+ const workspaceUuid = await this.getOrCreateWorkspaceUUID(workspaceRoot);
112
+ const storeLocation = await this.getStoreLocation(workspaceUuid, mangledKey);
113
+ const store = this.storeFactory();
114
+
115
+ store.initialize(
116
+ mangledKey,
117
+ storeLocation,
118
+ async () => this.resolveStoreLocation(mangledKey),
119
+ () => this.stores.delete(mangledKey)
120
+ );
121
+
122
+ this.stores.set(mangledKey, store);
123
+
124
+ this.logger.debug(`Created metadata store for key '${key}'`, {
125
+ mangledKey,
126
+ location: storeLocation.toString()
127
+ });
128
+
129
+ return store;
130
+ }
131
+
132
+ /**
133
+ * Mangles a key to make it safe for use as a directory name.
134
+ * Replaces all characters except alphanumerics, hyphens, and underscores with hyphens.
135
+ */
136
+ protected mangleKey(key: string): string {
137
+ return key.replace(/[^a-zA-Z0-9-_]/g, '-');
138
+ }
139
+
140
+ protected getFirstWorkspaceRoot(): URI | undefined {
141
+ const roots = this.workspaceService.tryGetRoots();
142
+ return roots.length > 0 ? roots[0].resource : undefined;
143
+ }
144
+
145
+ /**
146
+ * Gets or creates a UUID for the given workspace root.
147
+ * UUIDs are stored in an index file and reused if the same workspace is opened again.
148
+ */
149
+ protected async getOrCreateWorkspaceUUID(workspaceRoot: URI): Promise<string> {
150
+ const index = await this.loadIndex();
151
+ const workspacePath = workspaceRoot.path.toString();
152
+
153
+ if (index[workspacePath]) {
154
+ return index[workspacePath];
155
+ }
156
+
157
+ const newUuid = generateUuid();
158
+ index[workspacePath] = newUuid;
159
+
160
+ await this.saveIndex(index);
161
+
162
+ this.logger.debug('Generated new UUID for workspace', {
163
+ workspacePath,
164
+ uuid: newUuid
165
+ });
166
+
167
+ return newUuid;
168
+ }
169
+
170
+ protected async loadIndex(): Promise<WorkspaceMetadataIndex> {
171
+ const indexFileUri = await this.getIndexFile();
172
+
173
+ try {
174
+ const exists = await this.fileService.exists(indexFileUri);
175
+ if (!exists) {
176
+ return {};
177
+ }
178
+
179
+ const content = await this.fileService.readFile(indexFileUri);
180
+ return JSON.parse(content.value.toString()) as WorkspaceMetadataIndex;
181
+ } catch (error) {
182
+ this.logger.warn('Failed to load workspace metadata index, using empty index', error);
183
+ return {};
184
+ }
185
+ }
186
+
187
+ protected async saveIndex(index: WorkspaceMetadataIndex): Promise<void> {
188
+ const indexFileUri = await this.getIndexFile();
189
+
190
+ try {
191
+ // Ensure metadata root exists
192
+ const metadataRootUri = await this.getMetadataRoot();
193
+ await this.fileService.createFolder(metadataRootUri);
194
+
195
+ // Write index file
196
+ const content = JSON.stringify(index, undefined, 2);
197
+ await this.fileService.writeFile(
198
+ indexFileUri,
199
+ BinaryBuffer.fromString(content)
200
+ );
201
+ } catch (error) {
202
+ this.logger.error('Failed to save workspace metadata index', error);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ protected async getMetadataRoot(): Promise<URI> {
208
+ if (!this.metadataRoot) {
209
+ const configDirUri = await this.envVariableServer.getConfigDirUri();
210
+ this.metadataRoot = new URI(configDirUri).resolve('workspace-metadata');
211
+ }
212
+ return this.metadataRoot;
213
+ }
214
+
215
+ protected async getIndexFile(): Promise<URI> {
216
+ if (!this.indexFile) {
217
+ const metadataRoot = await this.getMetadataRoot();
218
+ this.indexFile = metadataRoot.resolve('index.json');
219
+ }
220
+ return this.indexFile;
221
+ }
222
+
223
+ /**
224
+ * Gets the location for a store given a workspace UUID and mangled key.
225
+ */
226
+ protected async getStoreLocation(workspaceUuid: string, mangledKey: string): Promise<URI> {
227
+ const metadataRoot = await this.getMetadataRoot();
228
+ return metadataRoot.resolve(workspaceUuid).resolve(mangledKey);
229
+ }
230
+
231
+ /**
232
+ * Resolves the current store location for a given mangled key.
233
+ * Used when workspace changes to get the new location.
234
+ */
235
+ protected async resolveStoreLocation(mangledKey: string): Promise<URI> {
236
+ const workspaceRoot = this.getFirstWorkspaceRoot();
237
+ if (!workspaceRoot) {
238
+ throw new Error('No workspace is currently open');
239
+ }
240
+
241
+ const workspaceUuid = await this.getOrCreateWorkspaceUUID(workspaceRoot);
242
+ return this.getStoreLocation(workspaceUuid, mangledKey);
243
+ }
244
+ }
@@ -0,0 +1,172 @@
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, named, postConstruct } from '@theia/core/shared/inversify';
18
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
19
+ import { ILogger, Emitter, Event, Disposable, DisposableCollection, URI } from '@theia/core/lib/common';
20
+ import { WorkspaceService } from '../workspace-service';
21
+
22
+ /**
23
+ * Represents a metadata store for a specific key within a workspace.
24
+ * The store provides access to a dedicated directory for storing workspace-specific metadata.
25
+ */
26
+ export interface WorkspaceMetadataStore extends Disposable {
27
+ /**
28
+ * The key identifying this metadata store.
29
+ */
30
+ readonly key: string;
31
+
32
+ /**
33
+ * The URI location of the metadata store directory.
34
+ */
35
+ readonly location: URI;
36
+
37
+ /**
38
+ * Event that fires when the location of the metadata store changes.
39
+ * It is the client's responsibility to reload or reinitialize any metadata from
40
+ * or in the new location.
41
+ */
42
+ readonly onDidChangeLocation: Event<URI>;
43
+
44
+ /**
45
+ * Ensures that the metadata store directory exists on disk.
46
+ * Creates the directory if it doesn't exist.
47
+ */
48
+ ensureExists(): Promise<void>;
49
+
50
+ /**
51
+ * Deletes the metadata store directory and all of its contents.
52
+ */
53
+ delete(): Promise<void>;
54
+ }
55
+
56
+ /**
57
+ * Implementation of WorkspaceMetadataStore.
58
+ * @internal
59
+ */
60
+ @injectable()
61
+ export class WorkspaceMetadataStoreImpl implements WorkspaceMetadataStore {
62
+
63
+ @inject(FileService)
64
+ protected readonly fileService: FileService;
65
+
66
+ @inject(WorkspaceService)
67
+ protected readonly workspaceService: WorkspaceService;
68
+
69
+ @inject(ILogger) @named('WorkspaceMetadataStorage')
70
+ protected readonly logger: ILogger;
71
+
72
+ protected readonly toDispose = new DisposableCollection();
73
+
74
+ protected readonly onDidChangeLocationEmitter = new Emitter<URI>();
75
+ readonly onDidChangeLocation: Event<URI> = this.onDidChangeLocationEmitter.event;
76
+
77
+ protected _location: URI;
78
+ protected _key: string;
79
+ protected currentWorkspaceRoot?: URI;
80
+ protected locationProvider: () => Promise<URI>;
81
+ protected onDisposeCallback?: () => void;
82
+
83
+ get location(): URI {
84
+ return this._location;
85
+ }
86
+
87
+ get key(): string {
88
+ return this._key;
89
+ }
90
+
91
+ /**
92
+ * Initializes the WorkspaceMetadataStore.
93
+ * @param key The key identifying this store
94
+ * @param initialLocation The initial location URI
95
+ * @param locationProvider Function to resolve the current location based on workspace changes
96
+ * @param onDispose Callback invoked when the store is disposed
97
+ */
98
+ initialize(key: string, initialLocation: URI, locationProvider: () => Promise<URI>, onDispose?: () => void): void {
99
+ this._key = key;
100
+ this._location = initialLocation;
101
+ this.locationProvider = locationProvider;
102
+ this.onDisposeCallback = onDispose;
103
+ this.currentWorkspaceRoot = this.getFirstWorkspaceRoot();
104
+ }
105
+
106
+ @postConstruct()
107
+ protected init(): void {
108
+ this.toDispose.push(this.onDidChangeLocationEmitter);
109
+ this.toDispose.push(
110
+ this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChange())
111
+ );
112
+ }
113
+
114
+ protected async handleWorkspaceChange(): Promise<void> {
115
+ const newWorkspaceRoot = this.getFirstWorkspaceRoot();
116
+
117
+ // Check if the first workspace root actually changed
118
+ if (this.currentWorkspaceRoot?.toString() !== newWorkspaceRoot?.toString()) {
119
+ this.currentWorkspaceRoot = newWorkspaceRoot;
120
+
121
+ try {
122
+ const newLocation = await this.locationProvider();
123
+ if (this._location.toString() !== newLocation.toString()) {
124
+ this._location = newLocation;
125
+ this.onDidChangeLocationEmitter.fire(newLocation);
126
+ this.logger.debug(`Metadata store location changed for key '${this._key}'`, {
127
+ newLocation: newLocation.toString()
128
+ });
129
+ }
130
+ } catch (error) {
131
+ this.logger.error(`Failed to update location for metadata store '${this._key}'`, error);
132
+ }
133
+ }
134
+ }
135
+
136
+ protected getFirstWorkspaceRoot(): URI | undefined {
137
+ const roots = this.workspaceService.tryGetRoots();
138
+ return roots.length > 0 ? roots[0].resource : undefined;
139
+ }
140
+
141
+ async ensureExists(): Promise<void> {
142
+ try {
143
+ await this.fileService.createFolder(this._location);
144
+ this.logger.debug(`Ensured metadata store exists for key '${this._key}'`, {
145
+ location: this._location.toString()
146
+ });
147
+ } catch (error) {
148
+ this.logger.error(`Failed to create metadata store directory for key '${this._key}'`, error);
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ async delete(): Promise<void> {
154
+ try {
155
+ const exists = await this.fileService.exists(this._location);
156
+ if (exists) {
157
+ await this.fileService.delete(this._location, { recursive: true, useTrash: false });
158
+ this.logger.debug(`Deleted metadata store for key '${this._key}'`, {
159
+ location: this._location.toString()
160
+ });
161
+ }
162
+ } catch (error) {
163
+ this.logger.error(`Failed to delete metadata store directory for key '${this._key}'`, error);
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ dispose(): void {
169
+ this.toDispose.dispose();
170
+ this.onDisposeCallback?.();
171
+ }
172
+ }
@@ -57,6 +57,8 @@ import { WorkspaceUserWorkingDirectoryProvider } from './workspace-user-working-
57
57
  import { WindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
58
58
  import { WorkspaceWindowTitleUpdater } from './workspace-window-title-updater';
59
59
  import { CanonicalUriService } from './canonical-uri-service';
60
+ import { WorkspaceMetadataStorageService, WorkspaceMetadataStorageServiceImpl, WorkspaceMetadataStoreFactory } from './metadata-storage';
61
+ import { WorkspaceMetadataStoreImpl } from './metadata-storage/workspace-metadata-store';
60
62
 
61
63
  export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
62
64
  bindWorkspacePreferences(bind);
@@ -101,6 +103,11 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
101
103
  bind(WorkspaceStorageService).toSelf().inSingletonScope();
102
104
  rebind(StorageService).toService(WorkspaceStorageService);
103
105
 
106
+ bind(WorkspaceMetadataStoreImpl).toSelf();
107
+ bind(WorkspaceMetadataStoreFactory).toFactory(ctx => () => ctx.container.get(WorkspaceMetadataStoreImpl));
108
+ bind(WorkspaceMetadataStorageServiceImpl).toSelf().inSingletonScope();
109
+ bind(WorkspaceMetadataStorageService).toService(WorkspaceMetadataStorageServiceImpl);
110
+
104
111
  bind(LabelProviderContribution).to(WorkspaceUriLabelProviderContribution).inSingletonScope();
105
112
  bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
106
113
  bind(VariableContribution).toService(WorkspaceVariableContribution);
@@ -394,12 +394,11 @@ export class WorkspaceTrustService {
394
394
  this.storage.setData(STORAGE_TRUSTED, undefined);
395
395
  }
396
396
 
397
- if (change.preferenceName === WORKSPACE_TRUST_ENABLED && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
398
- this.windowService.setSafeToShutDown();
399
- this.windowService.reload();
400
- }
401
-
402
397
  if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
398
+ if (!await this.isEmptyWorkspace() && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
399
+ this.windowService.setSafeToShutDown();
400
+ this.windowService.reload();
401
+ }
403
402
  this.resolveWorkspaceTrust();
404
403
  }
405
404