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
```