@theia/workspace
Version:
Theia - Workspace Extension
245 lines (203 loc) • 9.05 kB
text/typescript
/********************************************************************************
* Copyright (C) 2026 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { ILogger } from '@theia/core/lib/common/logger';
import { URI } from '@theia/core/lib/common/uri';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { WorkspaceService } from '../workspace-service';
import { WorkspaceMetadataStore, WorkspaceMetadataStoreImpl } from './workspace-metadata-store';
export const WorkspaceMetadataStoreFactory = Symbol('WorkspaceMetadataStoreFactory');
export type WorkspaceMetadataStoreFactory = () => WorkspaceMetadataStoreImpl;
/**
* Index mapping workspace root paths to UUIDs.
* Stored at $CONFIGDIR/workspace-metadata/index.json
*/
export interface WorkspaceMetadataIndex {
[workspacePath: string]: string; // workspace path -> UUID
}
/**
* Service for managing workspace-specific metadata storage.
* Provides isolated storage directories for different features within a workspace.
*
* This is different to the `WorkspaceStorageService` in that it is an unlimited free-form
* storage area _in the filesystem_ and not in the browser's local storage.
*/
export const WorkspaceMetadataStorageService = Symbol('WorkspaceMetadataStorageService');
export interface WorkspaceMetadataStorageService {
/**
* Gets an existing metadata store for the given key, or creates a new one if it doesn't exist.
*
* @param key A unique identifier for the metadata store. Special characters will be replaced with hyphens.
* @returns The existing or newly created WorkspaceMetadataStore instance
* @throws Error if no workspace is currently open
*/
getOrCreateStore(key: string): Promise<WorkspaceMetadataStore>;
}
export class WorkspaceMetadataStorageServiceImpl implements WorkspaceMetadataStorageService {
protected readonly fileService: FileService;
protected readonly workspaceService: WorkspaceService;
protected readonly envVariableServer: EnvVariablesServer;
protected readonly logger: ILogger;
protected readonly storeFactory: WorkspaceMetadataStoreFactory;
/**
* Registry of created stores by their mangled keys
*/
protected readonly stores = new Map<string, WorkspaceMetadataStore>();
/**
* Cached metadata root directory (e.g., file://$CONFIGDIR/workspace-metadata/)
*/
protected metadataRoot?: URI;
/**
* Cached index file location
*/
protected indexFile?: URI;
async getOrCreateStore(key: string): Promise<WorkspaceMetadataStore> {
const mangledKey = this.mangleKey(key);
const existingStore = this.stores.get(mangledKey);
if (existingStore) {
this.logger.debug(`Returning existing metadata store for key '${key}'`, {
mangledKey,
location: existingStore.location.toString()
});
return existingStore;
}
return this.doCreateStore(key, mangledKey);
}
protected async doCreateStore(key: string, mangledKey: string): Promise<WorkspaceMetadataStore> {
const workspaceRoot = this.getFirstWorkspaceRoot();
if (!workspaceRoot) {
throw new Error('Cannot create metadata store: no workspace is currently open');
}
const workspaceUuid = await this.getOrCreateWorkspaceUUID(workspaceRoot);
const storeLocation = await this.getStoreLocation(workspaceUuid, mangledKey);
const store = this.storeFactory();
store.initialize(
mangledKey,
storeLocation,
async () => this.resolveStoreLocation(mangledKey),
() => this.stores.delete(mangledKey)
);
this.stores.set(mangledKey, store);
this.logger.debug(`Created metadata store for key '${key}'`, {
mangledKey,
location: storeLocation.toString()
});
return store;
}
/**
* Mangles a key to make it safe for use as a directory name.
* Replaces all characters except alphanumerics, hyphens, and underscores with hyphens.
*/
protected mangleKey(key: string): string {
return key.replace(/[^a-zA-Z0-9-_]/g, '-');
}
protected getFirstWorkspaceRoot(): URI | undefined {
const roots = this.workspaceService.tryGetRoots();
return roots.length > 0 ? roots[0].resource : undefined;
}
/**
* Gets or creates a UUID for the given workspace root.
* UUIDs are stored in an index file and reused if the same workspace is opened again.
*/
protected async getOrCreateWorkspaceUUID(workspaceRoot: URI): Promise<string> {
const index = await this.loadIndex();
const workspacePath = workspaceRoot.path.toString();
if (index[workspacePath]) {
return index[workspacePath];
}
const newUuid = generateUuid();
index[workspacePath] = newUuid;
await this.saveIndex(index);
this.logger.debug('Generated new UUID for workspace', {
workspacePath,
uuid: newUuid
});
return newUuid;
}
protected async loadIndex(): Promise<WorkspaceMetadataIndex> {
const indexFileUri = await this.getIndexFile();
try {
const exists = await this.fileService.exists(indexFileUri);
if (!exists) {
return {};
}
const content = await this.fileService.readFile(indexFileUri);
return JSON.parse(content.value.toString()) as WorkspaceMetadataIndex;
} catch (error) {
this.logger.warn('Failed to load workspace metadata index, using empty index', error);
return {};
}
}
protected async saveIndex(index: WorkspaceMetadataIndex): Promise<void> {
const indexFileUri = await this.getIndexFile();
try {
// Ensure metadata root exists
const metadataRootUri = await this.getMetadataRoot();
await this.fileService.createFolder(metadataRootUri);
// Write index file
const content = JSON.stringify(index, undefined, 2);
await this.fileService.writeFile(
indexFileUri,
BinaryBuffer.fromString(content)
);
} catch (error) {
this.logger.error('Failed to save workspace metadata index', error);
throw error;
}
}
protected async getMetadataRoot(): Promise<URI> {
if (!this.metadataRoot) {
const configDirUri = await this.envVariableServer.getConfigDirUri();
this.metadataRoot = new URI(configDirUri).resolve('workspace-metadata');
}
return this.metadataRoot;
}
protected async getIndexFile(): Promise<URI> {
if (!this.indexFile) {
const metadataRoot = await this.getMetadataRoot();
this.indexFile = metadataRoot.resolve('index.json');
}
return this.indexFile;
}
/**
* Gets the location for a store given a workspace UUID and mangled key.
*/
protected async getStoreLocation(workspaceUuid: string, mangledKey: string): Promise<URI> {
const metadataRoot = await this.getMetadataRoot();
return metadataRoot.resolve(workspaceUuid).resolve(mangledKey);
}
/**
* Resolves the current store location for a given mangled key.
* Used when workspace changes to get the new location.
*/
protected async resolveStoreLocation(mangledKey: string): Promise<URI> {
const workspaceRoot = this.getFirstWorkspaceRoot();
if (!workspaceRoot) {
throw new Error('No workspace is currently open');
}
const workspaceUuid = await this.getOrCreateWorkspaceUUID(workspaceRoot);
return this.getStoreLocation(workspaceUuid, mangledKey);
}
}