Theia workspace extension providing workspace functionality and services for Eclipse Theia IDE framework
—
The @theia/workspace package provides several extension points that allow developers to customize and extend workspace functionality. These extension points enable integration of custom workspace handlers, command contributions, and workspace opening logic.
Interface for extending workspace opening logic with custom handlers.
/**
* Extension point for custom workspace opening handlers
*/
interface WorkspaceOpenHandlerContribution {
/**
* Check if this handler can handle the given URI
* @param uri - URI to check for handling capability
*/
canHandle(uri: URI): MaybePromise<boolean>;
/**
* Open workspace with the given URI and options
* @param uri - URI of the workspace to open
* @param options - Optional workspace input parameters
*/
openWorkspace(uri: URI, options?: WorkspaceInput): MaybePromise<void>;
/**
* Get workspace label for display purposes (optional)
* @param uri - URI of the workspace
*/
getWorkspaceLabel?(uri: URI): MaybePromise<string | undefined>;
}Usage Example:
import { injectable, inject } from "@theia/core/shared/inversify";
import { WorkspaceOpenHandlerContribution, WorkspaceInput } from "@theia/workspace/lib/browser";
import URI from "@theia/core/lib/common/uri";
import { MaybePromise } from "@theia/core";
@injectable()
export class RemoteWorkspaceOpenHandler implements WorkspaceOpenHandlerContribution {
canHandle(uri: URI): MaybePromise<boolean> {
// Handle custom remote workspace schemes
return uri.scheme === 'ssh' || uri.scheme === 'ftp' || uri.scheme === 'git';
}
async openWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {
console.log(`Opening remote workspace: ${uri}`);
if (uri.scheme === 'git') {
await this.openGitWorkspace(uri, options);
} else if (uri.scheme === 'ssh') {
await this.openSshWorkspace(uri, options);
} else if (uri.scheme === 'ftp') {
await this.openFtpWorkspace(uri, options);
}
}
getWorkspaceLabel(uri: URI): MaybePromise<string | undefined> {
// Provide custom labels for remote workspaces
switch (uri.scheme) {
case 'git':
return `Git: ${uri.path.base}`;
case 'ssh':
return `SSH: ${uri.authority}${uri.path}`;
case 'ftp':
return `FTP: ${uri.authority}${uri.path}`;
default:
return undefined;
}
}
private async openGitWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {
// Implementation for opening Git repositories as workspaces
console.log("Cloning and opening Git workspace...");
// 1. Clone repository to local temp directory
// 2. Open local directory as workspace
// 3. Set up Git integration
}
private async openSshWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {
// Implementation for SSH remote workspaces
console.log("Connecting to SSH workspace...");
// 1. Establish SSH connection
// 2. Mount remote filesystem
// 3. Open mounted directory as workspace
}
private async openFtpWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {
// Implementation for FTP workspaces
console.log("Connecting to FTP workspace...");
// 1. Establish FTP connection
// 2. Create virtual filesystem
// 3. Open as workspace
}
}
// Register the handler in your module
export default new ContainerModule(bind => {
bind(WorkspaceOpenHandlerContribution).to(RemoteWorkspaceOpenHandler).inSingletonScope();
});Interface for extending backend workspace validation and handling.
/**
* Backend extension point for custom workspace handlers
*/
interface WorkspaceHandlerContribution {
/**
* Check if this handler can handle the given URI scheme/format
* @param uri - URI to check for handling capability
*/
canHandle(uri: URI): boolean;
/**
* Check if the workspace still exists and is accessible
* @param uri - URI of the workspace to validate
*/
workspaceStillExists(uri: URI): Promise<boolean>;
}
/**
* Default file system workspace handler
*/
class FileWorkspaceHandlerContribution implements WorkspaceHandlerContribution {
/**
* Handles file:// scheme URIs
*/
canHandle(uri: URI): boolean;
/**
* Check if file/directory exists on disk
*/
workspaceStillExists(uri: URI): Promise<boolean>;
}Usage Example:
import { injectable, inject } from "@theia/core/shared/inversify";
import { WorkspaceHandlerContribution } from "@theia/workspace/lib/node";
import URI from "@theia/core/lib/common/uri";
@injectable()
export class DatabaseWorkspaceHandler implements WorkspaceHandlerContribution {
@inject(DatabaseConnectionService)
protected readonly dbService: DatabaseConnectionService;
canHandle(uri: URI): boolean {
// Handle database workspace schemes
return uri.scheme === 'mysql' || uri.scheme === 'postgres' || uri.scheme === 'mongodb';
}
async workspaceStillExists(uri: URI): Promise<boolean> {
try {
switch (uri.scheme) {
case 'mysql':
return await this.checkMysqlWorkspace(uri);
case 'postgres':
return await this.checkPostgresWorkspace(uri);
case 'mongodb':
return await this.checkMongoWorkspace(uri);
default:
return false;
}
} catch (error) {
console.error(`Failed to check database workspace ${uri}:`, error);
return false;
}
}
private async checkMysqlWorkspace(uri: URI): Promise<boolean> {
// Check if MySQL database/schema exists
const connection = await this.dbService.connect(uri);
try {
const exists = await connection.query(`SHOW DATABASES LIKE '${uri.path.base}'`);
return exists.length > 0;
} finally {
await connection.close();
}
}
private async checkPostgresWorkspace(uri: URI): Promise<boolean> {
// Check if PostgreSQL database exists
const connection = await this.dbService.connect(uri);
try {
const result = await connection.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[uri.path.base]
);
return result.rows.length > 0;
} finally {
await connection.close();
}
}
private async checkMongoWorkspace(uri: URI): Promise<boolean> {
// Check if MongoDB database exists
const client = await this.dbService.connectMongo(uri);
try {
const admin = client.db().admin();
const databases = await admin.listDatabases();
return databases.databases.some(db => db.name === uri.path.base);
} finally {
await client.close();
}
}
}
// Register in backend module
export default new ContainerModule(bind => {
bind(WorkspaceHandlerContribution).to(DatabaseWorkspaceHandler).inSingletonScope();
});Extension points for adding custom workspace commands and menu items.
/**
* Command contribution interface for workspace extensions
*/
interface CommandContribution {
/**
* Register custom commands
*/
registerCommands(registry: CommandRegistry): void;
}
/**
* Menu contribution interface for workspace extensions
*/
interface MenuContribution {
/**
* Register custom menu items
*/
registerMenus(registry: MenuModelRegistry): void;
}
/**
* URI command handler interface for file/folder operations
*/
interface UriCommandHandler<T> {
/**
* Execute command with URI(s)
*/
execute(uri: T, ...args: any[]): any;
/**
* Check if command is visible for given URI(s)
*/
isVisible?(uri: T, ...args: any[]): boolean;
/**
* Check if command is enabled for given URI(s)
*/
isEnabled?(uri: T, ...args: any[]): boolean;
}Usage Example:
import { injectable, inject } from "@theia/core/shared/inversify";
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from "@theia/core/lib/common";
import { UriCommandHandler } from "@theia/workspace/lib/browser";
import { CommonMenus } from "@theia/core/lib/browser";
import URI from "@theia/core/lib/common/uri";
// Custom command definitions
export namespace MyWorkspaceCommands {
export const ANALYZE_PROJECT: Command = {
id: 'my.workspace.analyze',
label: 'Analyze Project Structure'
};
export const GENERATE_DOCS: Command = {
id: 'my.workspace.generateDocs',
label: 'Generate Documentation'
};
export const OPTIMIZE_WORKSPACE: Command = {
id: 'my.workspace.optimize',
label: 'Optimize Workspace'
};
}
@injectable()
export class MyWorkspaceCommandContribution implements CommandContribution {
@inject(MyProjectAnalyzer)
protected readonly analyzer: MyProjectAnalyzer;
@inject(MyDocGenerator)
protected readonly docGenerator: MyDocGenerator;
registerCommands(registry: CommandRegistry): void {
// Register project analysis command
registry.registerCommand(MyWorkspaceCommands.ANALYZE_PROJECT, {
execute: () => this.analyzer.analyzeCurrentWorkspace(),
isEnabled: () => this.workspaceService.opened,
isVisible: () => true
});
// Register documentation generation command
registry.registerCommand(MyWorkspaceCommands.GENERATE_DOCS, new MyDocGenerationHandler());
// Register workspace optimization command
registry.registerCommand(MyWorkspaceCommands.OPTIMIZE_WORKSPACE, {
execute: async () => {
const roots = await this.workspaceService.roots;
await Promise.all(roots.map(root => this.optimizeRoot(root.uri)));
}
});
}
private async optimizeRoot(uri: URI): Promise<void> {
console.log(`Optimizing workspace root: ${uri}`);
// Custom optimization logic
}
}
@injectable()
export class MyDocGenerationHandler implements UriCommandHandler<URI> {
@inject(MyDocGenerator)
protected readonly docGenerator: MyDocGenerator;
async execute(uri: URI): Promise<void> {
console.log(`Generating documentation for: ${uri}`);
await this.docGenerator.generateForPath(uri);
}
isVisible(uri: URI): boolean {
// Only show for directories that contain source code
return this.docGenerator.hasSourceFiles(uri);
}
isEnabled(uri: URI): boolean {
// Enable if directory is writable
return this.docGenerator.isWritableDirectory(uri);
}
}
@injectable()
export class MyWorkspaceMenuContribution implements MenuContribution {
registerMenus(registry: MenuModelRegistry): void {
// Add to File menu
registry.registerMenuAction(CommonMenus.FILE, {
commandId: MyWorkspaceCommands.ANALYZE_PROJECT.id,
label: MyWorkspaceCommands.ANALYZE_PROJECT.label,
order: '6_workspace'
});
// Create custom submenu
const MY_WORKSPACE_MENU = [...CommonMenus.FILE, '7_my_workspace'];
registry.registerSubmenu(MY_WORKSPACE_MENU, 'My Workspace Tools');
// Add items to custom submenu
registry.registerMenuAction(MY_WORKSPACE_MENU, {
commandId: MyWorkspaceCommands.GENERATE_DOCS.id,
label: MyWorkspaceCommands.GENERATE_DOCS.label
});
registry.registerMenuAction(MY_WORKSPACE_MENU, {
commandId: MyWorkspaceCommands.OPTIMIZE_WORKSPACE.id,
label: MyWorkspaceCommands.OPTIMIZE_WORKSPACE.label
});
// Add to context menu for explorer
registry.registerMenuAction(['explorer-context-menu'], {
commandId: MyWorkspaceCommands.GENERATE_DOCS.id,
label: 'Generate Docs',
when: 'explorerResourceIsFolder'
});
}
}
// Register both contributions
export default new ContainerModule(bind => {
bind(CommandContribution).to(MyWorkspaceCommandContribution).inSingletonScope();
bind(MenuContribution).to(MyWorkspaceMenuContribution).inSingletonScope();
});Extension points for adding custom workspace preferences and configuration.
/**
* Preference contribution interface
*/
interface PreferenceContribution {
/**
* Preference schema definition
*/
readonly schema: PreferenceSchema;
}
/**
* Preference schema structure
*/
interface PreferenceSchema {
type: string;
scope?: PreferenceScope;
properties: { [key: string]: PreferenceSchemaProperty };
}Usage Example:
import { injectable } from "@theia/core/shared/inversify";
import { PreferenceContribution, PreferenceSchema, PreferenceScope } from "@theia/core/lib/browser";
// Define custom preference schema
export const myWorkspacePreferenceSchema: PreferenceSchema = {
type: 'object',
scope: PreferenceScope.Workspace, // Workspace-level preferences
properties: {
'myExtension.autoAnalyze': {
description: 'Automatically analyze project structure on workspace open',
type: 'boolean',
default: true
},
'myExtension.docFormat': {
description: 'Default documentation format',
type: 'string',
enum: ['markdown', 'html', 'pdf'],
default: 'markdown'
},
'myExtension.optimizationLevel': {
description: 'Workspace optimization level',
type: 'string',
enum: ['minimal', 'standard', 'aggressive'],
default: 'standard'
},
'myExtension.excludePatterns': {
description: 'File patterns to exclude from analysis',
type: 'array',
items: { type: 'string' },
default: ['node_modules/**', '.git/**', 'dist/**']
}
}
};
@injectable()
export class MyWorkspacePreferenceContribution implements PreferenceContribution {
readonly schema = myWorkspacePreferenceSchema;
}
// Usage of the preferences
interface MyWorkspaceConfiguration {
'myExtension.autoAnalyze': boolean;
'myExtension.docFormat': 'markdown' | 'html' | 'pdf';
'myExtension.optimizationLevel': 'minimal' | 'standard' | 'aggressive';
'myExtension.excludePatterns': string[];
}
type MyWorkspacePreferences = PreferenceProxy<MyWorkspaceConfiguration>;
@injectable()
export class MyWorkspaceConfigManager {
@inject(MyWorkspacePreferences)
protected readonly preferences: MyWorkspacePreferences;
shouldAutoAnalyze(): boolean {
return this.preferences['myExtension.autoAnalyze'];
}
getDocumentationFormat(): string {
return this.preferences['myExtension.docFormat'];
}
getExcludePatterns(): string[] {
return this.preferences['myExtension.excludePatterns'];
}
listenToPreferenceChanges(): void {
this.preferences.onPreferenceChanged(event => {
console.log(`Preference ${event.preferenceName} changed from ${event.oldValue} to ${event.newValue}`);
if (event.preferenceName === 'myExtension.autoAnalyze') {
this.handleAutoAnalyzeChange(event.newValue);
}
});
}
private handleAutoAnalyzeChange(enabled: boolean): void {
if (enabled) {
console.log("Auto-analysis enabled");
this.startAutoAnalysis();
} else {
console.log("Auto-analysis disabled");
this.stopAutoAnalysis();
}
}
}
// Register preference contribution
export default new ContainerModule(bind => {
bind(PreferenceContribution).to(MyWorkspacePreferenceContribution).inSingletonScope();
bind(MyWorkspacePreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get(PreferenceService);
return createPreferenceProxy(preferences, myWorkspacePreferenceSchema);
}).inSingletonScope();
});Extension points for reacting to workspace events and lifecycle changes.
Usage Example:
import { injectable, inject, postConstruct } from "@theia/core/shared/inversify";
import { WorkspaceService, DidCreateNewResourceEvent } from "@theia/workspace/lib/browser";
import { Disposable, DisposableCollection } from "@theia/core";
@injectable()
export class MyWorkspaceEventListener {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
protected readonly disposables = new DisposableCollection();
@postConstruct()
initialize(): void {
this.listenToWorkspaceEvents();
}
dispose(): void {
this.disposables.dispose();
}
private listenToWorkspaceEvents(): void {
// Listen to workspace changes
this.disposables.push(
this.workspaceService.onWorkspaceChanged(roots => {
console.log(`Workspace roots changed. New count: ${roots.length}`);
this.handleWorkspaceChange(roots);
})
);
// Listen to workspace location changes
this.disposables.push(
this.workspaceService.onWorkspaceLocationChanged(workspace => {
if (workspace) {
console.log(`Workspace location changed to: ${workspace.uri}`);
this.handleWorkspaceLocationChange(workspace);
} else {
console.log("Workspace was closed");
this.handleWorkspaceClosed();
}
})
);
// Listen to new file/folder creation
this.disposables.push(
this.workspaceCommands.onDidCreateNewFile(event => {
console.log(`New file created: ${event.uri}`);
this.handleNewFileCreated(event);
})
);
this.disposables.push(
this.workspaceCommands.onDidCreateNewFolder(event => {
console.log(`New folder created: ${event.uri}`);
this.handleNewFolderCreated(event);
})
);
}
private handleWorkspaceChange(roots: FileStat[]): void {
// React to workspace root changes
roots.forEach((root, index) => {
console.log(`Processing root ${index}: ${root.uri}`);
this.analyzeWorkspaceRoot(root);
});
}
private handleWorkspaceLocationChange(workspace: FileStat): void {
// React to workspace file location changes
this.updateWorkspaceConfiguration(workspace);
}
private handleWorkspaceClosed(): void {
// Clean up when workspace is closed
this.cleanupWorkspaceData();
}
private handleNewFileCreated(event: DidCreateNewResourceEvent): void {
// Automatically configure new files
this.configureNewFile(event.uri, event.parent);
}
private handleNewFolderCreated(event: DidCreateNewResourceEvent): void {
// Set up new folder structure
this.initializeFolderStructure(event.uri);
}
}interface WorkspaceOpenHandlerContribution {
canHandle(uri: URI): MaybePromise<boolean>;
openWorkspace(uri: URI, options?: WorkspaceInput): MaybePromise<void>;
getWorkspaceLabel?(uri: URI): MaybePromise<string | undefined>;
}
interface WorkspaceHandlerContribution {
canHandle(uri: URI): boolean;
workspaceStillExists(uri: URI): Promise<boolean>;
}
interface CommandContribution {
registerCommands(registry: CommandRegistry): void;
}
interface MenuContribution {
registerMenus(registry: MenuModelRegistry): void;
}
interface UriCommandHandler<T> {
execute(uri: T, ...args: any[]): any;
isVisible?(uri: T, ...args: any[]): boolean;
isEnabled?(uri: T, ...args: any[]): boolean;
}
interface PreferenceContribution {
readonly schema: PreferenceSchema;
}
interface WorkspaceInput {
preserveWindow?: boolean;
}
type MaybePromise<T> = T | Promise<T>;
// Contribution provider symbols
const WorkspaceOpenHandlerContribution: symbol;
const WorkspaceHandlerContribution: symbol;Install with Tessl CLI
npx tessl i tessl/npm-theia--workspace