or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

extension-points.mdindex.mdworkspace-commands.mdworkspace-file-handling.mdworkspace-preferences.mdworkspace-server.mdworkspace-service.md

extension-points.mddocs/

0

# Extension Points

1

2

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.

3

4

## Capabilities

5

6

### Workspace Opening Handler Extension

7

8

Interface for extending workspace opening logic with custom handlers.

9

10

```typescript { .api }

11

/**

12

* Extension point for custom workspace opening handlers

13

*/

14

interface WorkspaceOpenHandlerContribution {

15

/**

16

* Check if this handler can handle the given URI

17

* @param uri - URI to check for handling capability

18

*/

19

canHandle(uri: URI): MaybePromise<boolean>;

20

21

/**

22

* Open workspace with the given URI and options

23

* @param uri - URI of the workspace to open

24

* @param options - Optional workspace input parameters

25

*/

26

openWorkspace(uri: URI, options?: WorkspaceInput): MaybePromise<void>;

27

28

/**

29

* Get workspace label for display purposes (optional)

30

* @param uri - URI of the workspace

31

*/

32

getWorkspaceLabel?(uri: URI): MaybePromise<string | undefined>;

33

}

34

```

35

36

**Usage Example:**

37

38

```typescript

39

import { injectable, inject } from "@theia/core/shared/inversify";

40

import { WorkspaceOpenHandlerContribution, WorkspaceInput } from "@theia/workspace/lib/browser";

41

import URI from "@theia/core/lib/common/uri";

42

import { MaybePromise } from "@theia/core";

43

44

@injectable()

45

export class RemoteWorkspaceOpenHandler implements WorkspaceOpenHandlerContribution {

46

47

canHandle(uri: URI): MaybePromise<boolean> {

48

// Handle custom remote workspace schemes

49

return uri.scheme === 'ssh' || uri.scheme === 'ftp' || uri.scheme === 'git';

50

}

51

52

async openWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {

53

console.log(`Opening remote workspace: ${uri}`);

54

55

if (uri.scheme === 'git') {

56

await this.openGitWorkspace(uri, options);

57

} else if (uri.scheme === 'ssh') {

58

await this.openSshWorkspace(uri, options);

59

} else if (uri.scheme === 'ftp') {

60

await this.openFtpWorkspace(uri, options);

61

}

62

}

63

64

getWorkspaceLabel(uri: URI): MaybePromise<string | undefined> {

65

// Provide custom labels for remote workspaces

66

switch (uri.scheme) {

67

case 'git':

68

return `Git: ${uri.path.base}`;

69

case 'ssh':

70

return `SSH: ${uri.authority}${uri.path}`;

71

case 'ftp':

72

return `FTP: ${uri.authority}${uri.path}`;

73

default:

74

return undefined;

75

}

76

}

77

78

private async openGitWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {

79

// Implementation for opening Git repositories as workspaces

80

console.log("Cloning and opening Git workspace...");

81

// 1. Clone repository to local temp directory

82

// 2. Open local directory as workspace

83

// 3. Set up Git integration

84

}

85

86

private async openSshWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {

87

// Implementation for SSH remote workspaces

88

console.log("Connecting to SSH workspace...");

89

// 1. Establish SSH connection

90

// 2. Mount remote filesystem

91

// 3. Open mounted directory as workspace

92

}

93

94

private async openFtpWorkspace(uri: URI, options?: WorkspaceInput): Promise<void> {

95

// Implementation for FTP workspaces

96

console.log("Connecting to FTP workspace...");

97

// 1. Establish FTP connection

98

// 2. Create virtual filesystem

99

// 3. Open as workspace

100

}

101

}

102

103

// Register the handler in your module

104

export default new ContainerModule(bind => {

105

bind(WorkspaceOpenHandlerContribution).to(RemoteWorkspaceOpenHandler).inSingletonScope();

106

});

107

```

108

109

### Backend Workspace Handler Extension

110

111

Interface for extending backend workspace validation and handling.

112

113

```typescript { .api }

114

/**

115

* Backend extension point for custom workspace handlers

116

*/

117

interface WorkspaceHandlerContribution {

118

/**

119

* Check if this handler can handle the given URI scheme/format

120

* @param uri - URI to check for handling capability

121

*/

122

canHandle(uri: URI): boolean;

123

124

/**

125

* Check if the workspace still exists and is accessible

126

* @param uri - URI of the workspace to validate

127

*/

128

workspaceStillExists(uri: URI): Promise<boolean>;

129

}

130

131

/**

132

* Default file system workspace handler

133

*/

134

class FileWorkspaceHandlerContribution implements WorkspaceHandlerContribution {

135

/**

136

* Handles file:// scheme URIs

137

*/

138

canHandle(uri: URI): boolean;

139

140

/**

141

* Check if file/directory exists on disk

142

*/

143

workspaceStillExists(uri: URI): Promise<boolean>;

144

}

145

```

146

147

**Usage Example:**

148

149

```typescript

150

import { injectable, inject } from "@theia/core/shared/inversify";

151

import { WorkspaceHandlerContribution } from "@theia/workspace/lib/node";

152

import URI from "@theia/core/lib/common/uri";

153

154

@injectable()

155

export class DatabaseWorkspaceHandler implements WorkspaceHandlerContribution {

156

157

@inject(DatabaseConnectionService)

158

protected readonly dbService: DatabaseConnectionService;

159

160

canHandle(uri: URI): boolean {

161

// Handle database workspace schemes

162

return uri.scheme === 'mysql' || uri.scheme === 'postgres' || uri.scheme === 'mongodb';

163

}

164

165

async workspaceStillExists(uri: URI): Promise<boolean> {

166

try {

167

switch (uri.scheme) {

168

case 'mysql':

169

return await this.checkMysqlWorkspace(uri);

170

case 'postgres':

171

return await this.checkPostgresWorkspace(uri);

172

case 'mongodb':

173

return await this.checkMongoWorkspace(uri);

174

default:

175

return false;

176

}

177

} catch (error) {

178

console.error(`Failed to check database workspace ${uri}:`, error);

179

return false;

180

}

181

}

182

183

private async checkMysqlWorkspace(uri: URI): Promise<boolean> {

184

// Check if MySQL database/schema exists

185

const connection = await this.dbService.connect(uri);

186

try {

187

const exists = await connection.query(`SHOW DATABASES LIKE '${uri.path.base}'`);

188

return exists.length > 0;

189

} finally {

190

await connection.close();

191

}

192

}

193

194

private async checkPostgresWorkspace(uri: URI): Promise<boolean> {

195

// Check if PostgreSQL database exists

196

const connection = await this.dbService.connect(uri);

197

try {

198

const result = await connection.query(

199

`SELECT 1 FROM pg_database WHERE datname = $1`,

200

[uri.path.base]

201

);

202

return result.rows.length > 0;

203

} finally {

204

await connection.close();

205

}

206

}

207

208

private async checkMongoWorkspace(uri: URI): Promise<boolean> {

209

// Check if MongoDB database exists

210

const client = await this.dbService.connectMongo(uri);

211

try {

212

const admin = client.db().admin();

213

const databases = await admin.listDatabases();

214

return databases.databases.some(db => db.name === uri.path.base);

215

} finally {

216

await client.close();

217

}

218

}

219

}

220

221

// Register in backend module

222

export default new ContainerModule(bind => {

223

bind(WorkspaceHandlerContribution).to(DatabaseWorkspaceHandler).inSingletonScope();

224

});

225

```

226

227

### Command and Menu Extension Points

228

229

Extension points for adding custom workspace commands and menu items.

230

231

```typescript { .api }

232

/**

233

* Command contribution interface for workspace extensions

234

*/

235

interface CommandContribution {

236

/**

237

* Register custom commands

238

*/

239

registerCommands(registry: CommandRegistry): void;

240

}

241

242

/**

243

* Menu contribution interface for workspace extensions

244

*/

245

interface MenuContribution {

246

/**

247

* Register custom menu items

248

*/

249

registerMenus(registry: MenuModelRegistry): void;

250

}

251

252

/**

253

* URI command handler interface for file/folder operations

254

*/

255

interface UriCommandHandler<T> {

256

/**

257

* Execute command with URI(s)

258

*/

259

execute(uri: T, ...args: any[]): any;

260

261

/**

262

* Check if command is visible for given URI(s)

263

*/

264

isVisible?(uri: T, ...args: any[]): boolean;

265

266

/**

267

* Check if command is enabled for given URI(s)

268

*/

269

isEnabled?(uri: T, ...args: any[]): boolean;

270

}

271

```

272

273

**Usage Example:**

274

275

```typescript

276

import { injectable, inject } from "@theia/core/shared/inversify";

277

import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from "@theia/core/lib/common";

278

import { UriCommandHandler } from "@theia/workspace/lib/browser";

279

import { CommonMenus } from "@theia/core/lib/browser";

280

import URI from "@theia/core/lib/common/uri";

281

282

// Custom command definitions

283

export namespace MyWorkspaceCommands {

284

export const ANALYZE_PROJECT: Command = {

285

id: 'my.workspace.analyze',

286

label: 'Analyze Project Structure'

287

};

288

289

export const GENERATE_DOCS: Command = {

290

id: 'my.workspace.generateDocs',

291

label: 'Generate Documentation'

292

};

293

294

export const OPTIMIZE_WORKSPACE: Command = {

295

id: 'my.workspace.optimize',

296

label: 'Optimize Workspace'

297

};

298

}

299

300

@injectable()

301

export class MyWorkspaceCommandContribution implements CommandContribution {

302

303

@inject(MyProjectAnalyzer)

304

protected readonly analyzer: MyProjectAnalyzer;

305

306

@inject(MyDocGenerator)

307

protected readonly docGenerator: MyDocGenerator;

308

309

registerCommands(registry: CommandRegistry): void {

310

// Register project analysis command

311

registry.registerCommand(MyWorkspaceCommands.ANALYZE_PROJECT, {

312

execute: () => this.analyzer.analyzeCurrentWorkspace(),

313

isEnabled: () => this.workspaceService.opened,

314

isVisible: () => true

315

});

316

317

// Register documentation generation command

318

registry.registerCommand(MyWorkspaceCommands.GENERATE_DOCS, new MyDocGenerationHandler());

319

320

// Register workspace optimization command

321

registry.registerCommand(MyWorkspaceCommands.OPTIMIZE_WORKSPACE, {

322

execute: async () => {

323

const roots = await this.workspaceService.roots;

324

await Promise.all(roots.map(root => this.optimizeRoot(root.uri)));

325

}

326

});

327

}

328

329

private async optimizeRoot(uri: URI): Promise<void> {

330

console.log(`Optimizing workspace root: ${uri}`);

331

// Custom optimization logic

332

}

333

}

334

335

@injectable()

336

export class MyDocGenerationHandler implements UriCommandHandler<URI> {

337

338

@inject(MyDocGenerator)

339

protected readonly docGenerator: MyDocGenerator;

340

341

async execute(uri: URI): Promise<void> {

342

console.log(`Generating documentation for: ${uri}`);

343

await this.docGenerator.generateForPath(uri);

344

}

345

346

isVisible(uri: URI): boolean {

347

// Only show for directories that contain source code

348

return this.docGenerator.hasSourceFiles(uri);

349

}

350

351

isEnabled(uri: URI): boolean {

352

// Enable if directory is writable

353

return this.docGenerator.isWritableDirectory(uri);

354

}

355

}

356

357

@injectable()

358

export class MyWorkspaceMenuContribution implements MenuContribution {

359

360

registerMenus(registry: MenuModelRegistry): void {

361

// Add to File menu

362

registry.registerMenuAction(CommonMenus.FILE, {

363

commandId: MyWorkspaceCommands.ANALYZE_PROJECT.id,

364

label: MyWorkspaceCommands.ANALYZE_PROJECT.label,

365

order: '6_workspace'

366

});

367

368

// Create custom submenu

369

const MY_WORKSPACE_MENU = [...CommonMenus.FILE, '7_my_workspace'];

370

registry.registerSubmenu(MY_WORKSPACE_MENU, 'My Workspace Tools');

371

372

// Add items to custom submenu

373

registry.registerMenuAction(MY_WORKSPACE_MENU, {

374

commandId: MyWorkspaceCommands.GENERATE_DOCS.id,

375

label: MyWorkspaceCommands.GENERATE_DOCS.label

376

});

377

378

registry.registerMenuAction(MY_WORKSPACE_MENU, {

379

commandId: MyWorkspaceCommands.OPTIMIZE_WORKSPACE.id,

380

label: MyWorkspaceCommands.OPTIMIZE_WORKSPACE.label

381

});

382

383

// Add to context menu for explorer

384

registry.registerMenuAction(['explorer-context-menu'], {

385

commandId: MyWorkspaceCommands.GENERATE_DOCS.id,

386

label: 'Generate Docs',

387

when: 'explorerResourceIsFolder'

388

});

389

}

390

}

391

392

// Register both contributions

393

export default new ContainerModule(bind => {

394

bind(CommandContribution).to(MyWorkspaceCommandContribution).inSingletonScope();

395

bind(MenuContribution).to(MyWorkspaceMenuContribution).inSingletonScope();

396

});

397

```

398

399

### Preference Extension Points

400

401

Extension points for adding custom workspace preferences and configuration.

402

403

```typescript { .api }

404

/**

405

* Preference contribution interface

406

*/

407

interface PreferenceContribution {

408

/**

409

* Preference schema definition

410

*/

411

readonly schema: PreferenceSchema;

412

}

413

414

/**

415

* Preference schema structure

416

*/

417

interface PreferenceSchema {

418

type: string;

419

scope?: PreferenceScope;

420

properties: { [key: string]: PreferenceSchemaProperty };

421

}

422

```

423

424

**Usage Example:**

425

426

```typescript

427

import { injectable } from "@theia/core/shared/inversify";

428

import { PreferenceContribution, PreferenceSchema, PreferenceScope } from "@theia/core/lib/browser";

429

430

// Define custom preference schema

431

export const myWorkspacePreferenceSchema: PreferenceSchema = {

432

type: 'object',

433

scope: PreferenceScope.Workspace, // Workspace-level preferences

434

properties: {

435

'myExtension.autoAnalyze': {

436

description: 'Automatically analyze project structure on workspace open',

437

type: 'boolean',

438

default: true

439

},

440

'myExtension.docFormat': {

441

description: 'Default documentation format',

442

type: 'string',

443

enum: ['markdown', 'html', 'pdf'],

444

default: 'markdown'

445

},

446

'myExtension.optimizationLevel': {

447

description: 'Workspace optimization level',

448

type: 'string',

449

enum: ['minimal', 'standard', 'aggressive'],

450

default: 'standard'

451

},

452

'myExtension.excludePatterns': {

453

description: 'File patterns to exclude from analysis',

454

type: 'array',

455

items: { type: 'string' },

456

default: ['node_modules/**', '.git/**', 'dist/**']

457

}

458

}

459

};

460

461

@injectable()

462

export class MyWorkspacePreferenceContribution implements PreferenceContribution {

463

readonly schema = myWorkspacePreferenceSchema;

464

}

465

466

// Usage of the preferences

467

interface MyWorkspaceConfiguration {

468

'myExtension.autoAnalyze': boolean;

469

'myExtension.docFormat': 'markdown' | 'html' | 'pdf';

470

'myExtension.optimizationLevel': 'minimal' | 'standard' | 'aggressive';

471

'myExtension.excludePatterns': string[];

472

}

473

474

type MyWorkspacePreferences = PreferenceProxy<MyWorkspaceConfiguration>;

475

476

@injectable()

477

export class MyWorkspaceConfigManager {

478

479

@inject(MyWorkspacePreferences)

480

protected readonly preferences: MyWorkspacePreferences;

481

482

shouldAutoAnalyze(): boolean {

483

return this.preferences['myExtension.autoAnalyze'];

484

}

485

486

getDocumentationFormat(): string {

487

return this.preferences['myExtension.docFormat'];

488

}

489

490

getExcludePatterns(): string[] {

491

return this.preferences['myExtension.excludePatterns'];

492

}

493

494

listenToPreferenceChanges(): void {

495

this.preferences.onPreferenceChanged(event => {

496

console.log(`Preference ${event.preferenceName} changed from ${event.oldValue} to ${event.newValue}`);

497

498

if (event.preferenceName === 'myExtension.autoAnalyze') {

499

this.handleAutoAnalyzeChange(event.newValue);

500

}

501

});

502

}

503

504

private handleAutoAnalyzeChange(enabled: boolean): void {

505

if (enabled) {

506

console.log("Auto-analysis enabled");

507

this.startAutoAnalysis();

508

} else {

509

console.log("Auto-analysis disabled");

510

this.stopAutoAnalysis();

511

}

512

}

513

}

514

515

// Register preference contribution

516

export default new ContainerModule(bind => {

517

bind(PreferenceContribution).to(MyWorkspacePreferenceContribution).inSingletonScope();

518

bind(MyWorkspacePreferences).toDynamicValue(ctx => {

519

const preferences = ctx.container.get(PreferenceService);

520

return createPreferenceProxy(preferences, myWorkspacePreferenceSchema);

521

}).inSingletonScope();

522

});

523

```

524

525

### Event-Based Extensions

526

527

Extension points for reacting to workspace events and lifecycle changes.

528

529

**Usage Example:**

530

531

```typescript

532

import { injectable, inject, postConstruct } from "@theia/core/shared/inversify";

533

import { WorkspaceService, DidCreateNewResourceEvent } from "@theia/workspace/lib/browser";

534

import { Disposable, DisposableCollection } from "@theia/core";

535

536

@injectable()

537

export class MyWorkspaceEventListener {

538

539

@inject(WorkspaceService)

540

protected readonly workspaceService: WorkspaceService;

541

542

protected readonly disposables = new DisposableCollection();

543

544

@postConstruct()

545

initialize(): void {

546

this.listenToWorkspaceEvents();

547

}

548

549

dispose(): void {

550

this.disposables.dispose();

551

}

552

553

private listenToWorkspaceEvents(): void {

554

// Listen to workspace changes

555

this.disposables.push(

556

this.workspaceService.onWorkspaceChanged(roots => {

557

console.log(`Workspace roots changed. New count: ${roots.length}`);

558

this.handleWorkspaceChange(roots);

559

})

560

);

561

562

// Listen to workspace location changes

563

this.disposables.push(

564

this.workspaceService.onWorkspaceLocationChanged(workspace => {

565

if (workspace) {

566

console.log(`Workspace location changed to: ${workspace.uri}`);

567

this.handleWorkspaceLocationChange(workspace);

568

} else {

569

console.log("Workspace was closed");

570

this.handleWorkspaceClosed();

571

}

572

})

573

);

574

575

// Listen to new file/folder creation

576

this.disposables.push(

577

this.workspaceCommands.onDidCreateNewFile(event => {

578

console.log(`New file created: ${event.uri}`);

579

this.handleNewFileCreated(event);

580

})

581

);

582

583

this.disposables.push(

584

this.workspaceCommands.onDidCreateNewFolder(event => {

585

console.log(`New folder created: ${event.uri}`);

586

this.handleNewFolderCreated(event);

587

})

588

);

589

}

590

591

private handleWorkspaceChange(roots: FileStat[]): void {

592

// React to workspace root changes

593

roots.forEach((root, index) => {

594

console.log(`Processing root ${index}: ${root.uri}`);

595

this.analyzeWorkspaceRoot(root);

596

});

597

}

598

599

private handleWorkspaceLocationChange(workspace: FileStat): void {

600

// React to workspace file location changes

601

this.updateWorkspaceConfiguration(workspace);

602

}

603

604

private handleWorkspaceClosed(): void {

605

// Clean up when workspace is closed

606

this.cleanupWorkspaceData();

607

}

608

609

private handleNewFileCreated(event: DidCreateNewResourceEvent): void {

610

// Automatically configure new files

611

this.configureNewFile(event.uri, event.parent);

612

}

613

614

private handleNewFolderCreated(event: DidCreateNewResourceEvent): void {

615

// Set up new folder structure

616

this.initializeFolderStructure(event.uri);

617

}

618

}

619

```

620

621

## Types

622

623

```typescript { .api }

624

interface WorkspaceOpenHandlerContribution {

625

canHandle(uri: URI): MaybePromise<boolean>;

626

openWorkspace(uri: URI, options?: WorkspaceInput): MaybePromise<void>;

627

getWorkspaceLabel?(uri: URI): MaybePromise<string | undefined>;

628

}

629

630

interface WorkspaceHandlerContribution {

631

canHandle(uri: URI): boolean;

632

workspaceStillExists(uri: URI): Promise<boolean>;

633

}

634

635

interface CommandContribution {

636

registerCommands(registry: CommandRegistry): void;

637

}

638

639

interface MenuContribution {

640

registerMenus(registry: MenuModelRegistry): void;

641

}

642

643

interface UriCommandHandler<T> {

644

execute(uri: T, ...args: any[]): any;

645

isVisible?(uri: T, ...args: any[]): boolean;

646

isEnabled?(uri: T, ...args: any[]): boolean;

647

}

648

649

interface PreferenceContribution {

650

readonly schema: PreferenceSchema;

651

}

652

653

interface WorkspaceInput {

654

preserveWindow?: boolean;

655

}

656

657

type MaybePromise<T> = T | Promise<T>;

658

659

// Contribution provider symbols

660

const WorkspaceOpenHandlerContribution: symbol;

661

const WorkspaceHandlerContribution: symbol;

662

```