CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-theia--workspace

Theia workspace extension providing workspace functionality and services for Eclipse Theia IDE framework

Pending
Overview
Eval results
Files

extension-points.mddocs/

Extension Points

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.

Capabilities

Workspace Opening Handler Extension

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();
});

Backend Workspace Handler Extension

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();
});

Command and Menu Extension Points

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();
});

Preference Extension Points

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();
});

Event-Based Extensions

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);
  }
}

Types

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

docs

extension-points.md

index.md

workspace-commands.md

workspace-file-handling.md

workspace-preferences.md

workspace-server.md

workspace-service.md

tile.json