@theia/getting-started 1.53.0-next.4 → 1.53.0-next.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,550 +1,628 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2018 Ericsson and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import * as React from '@theia/core/shared/react';
18
- import URI from '@theia/core/lib/common/uri';
19
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
20
- import { CommandRegistry, isOSX, environment, Path } from '@theia/core/lib/common';
21
- import { WorkspaceCommands, WorkspaceService } from '@theia/workspace/lib/browser';
22
- import { KeymapsCommands } from '@theia/keymaps/lib/browser';
23
- import { Message, ReactWidget, CommonCommands, LabelProvider, Key, KeyCode, codicon, PreferenceService } from '@theia/core/lib/browser';
24
- import { ApplicationInfo, ApplicationServer } from '@theia/core/lib/common/application-protocol';
25
- import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
26
- import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
27
- import { WindowService } from '@theia/core/lib/browser/window/window-service';
28
- import { nls } from '@theia/core/lib/common/nls';
29
-
30
- /**
31
- * Default implementation of the `GettingStartedWidget`.
32
- * The widget is displayed when there are currently no workspaces present.
33
- * Some of the features displayed include:
34
- * - `open` commands.
35
- * - `recently used workspaces`.
36
- * - `settings` commands.
37
- * - `help` commands.
38
- * - helpful links.
39
- */
40
- @injectable()
41
- export class GettingStartedWidget extends ReactWidget {
42
-
43
- /**
44
- * The widget `id`.
45
- */
46
- static readonly ID = 'getting.started.widget';
47
- /**
48
- * The widget `label` which is used for display purposes.
49
- */
50
- static readonly LABEL = nls.localizeByDefault('Welcome');
51
-
52
- /**
53
- * The `ApplicationInfo` for the application if available.
54
- * Used in order to obtain the version number of the application.
55
- */
56
- protected applicationInfo: ApplicationInfo | undefined;
57
- /**
58
- * The application name which is used for display purposes.
59
- */
60
- protected applicationName = FrontendApplicationConfigProvider.get().applicationName;
61
-
62
- protected home: string | undefined;
63
-
64
- /**
65
- * The recently used workspaces limit.
66
- * Used in order to limit the number of recently used workspaces to display.
67
- */
68
- protected recentLimit = 5;
69
- /**
70
- * The list of recently used workspaces.
71
- */
72
- protected recentWorkspaces: string[] = [];
73
-
74
- /**
75
- * Collection of useful links to display for end users.
76
- */
77
- protected readonly documentationUrl = 'https://www.theia-ide.org/docs/';
78
- protected readonly compatibilityUrl = 'https://eclipse-theia.github.io/vscode-theia-comparator/status.html';
79
- protected readonly extensionUrl = 'https://www.theia-ide.org/docs/authoring_extensions';
80
- protected readonly pluginUrl = 'https://www.theia-ide.org/docs/authoring_plugins';
81
-
82
- @inject(ApplicationServer)
83
- protected readonly appServer: ApplicationServer;
84
-
85
- @inject(CommandRegistry)
86
- protected readonly commandRegistry: CommandRegistry;
87
-
88
- @inject(EnvVariablesServer)
89
- protected readonly environments: EnvVariablesServer;
90
-
91
- @inject(LabelProvider)
92
- protected readonly labelProvider: LabelProvider;
93
-
94
- @inject(WindowService)
95
- protected readonly windowService: WindowService;
96
-
97
- @inject(WorkspaceService)
98
- protected readonly workspaceService: WorkspaceService;
99
-
100
- @inject(PreferenceService)
101
- protected readonly preferenceService: PreferenceService;
102
-
103
- @postConstruct()
104
- protected init(): void {
105
- this.doInit();
106
- }
107
-
108
- protected async doInit(): Promise<void> {
109
- this.id = GettingStartedWidget.ID;
110
- this.title.label = GettingStartedWidget.LABEL;
111
- this.title.caption = GettingStartedWidget.LABEL;
112
- this.title.closable = true;
113
-
114
- this.applicationInfo = await this.appServer.getApplicationInfo();
115
- this.recentWorkspaces = await this.workspaceService.recentWorkspaces();
116
- this.home = new URI(await this.environments.getHomeDirUri()).path.toString();
117
- this.update();
118
- }
119
-
120
- protected override onActivateRequest(msg: Message): void {
121
- super.onActivateRequest(msg);
122
- const elArr = this.node.getElementsByTagName('a');
123
- if (elArr && elArr.length > 0) {
124
- (elArr[0] as HTMLElement).focus();
125
- }
126
- }
127
-
128
- /**
129
- * Render the content of the widget.
130
- */
131
- protected render(): React.ReactNode {
132
- return <div className='gs-container'>
133
- <div className='gs-content-container'>
134
- {this.renderHeader()}
135
- <hr className='gs-hr' />
136
- <div className='flex-grid'>
137
- <div className='col'>
138
- {this.renderStart()}
139
- </div>
140
- </div>
141
- <div className='flex-grid'>
142
- <div className='col'>
143
- {this.renderRecentWorkspaces()}
144
- </div>
145
- </div>
146
- <div className='flex-grid'>
147
- <div className='col'>
148
- {this.renderSettings()}
149
- </div>
150
- </div>
151
- <div className='flex-grid'>
152
- <div className='col'>
153
- {this.renderHelp()}
154
- </div>
155
- </div>
156
- <div className='flex-grid'>
157
- <div className='col'>
158
- {this.renderVersion()}
159
- </div>
160
- </div>
161
- </div>
162
- <div className='gs-preference-container'>
163
- {this.renderPreferences()}
164
- </div>
165
- </div>;
166
- }
167
-
168
- /**
169
- * Render the widget header.
170
- * Renders the title `{applicationName} Getting Started`.
171
- */
172
- protected renderHeader(): React.ReactNode {
173
- return <div className='gs-header'>
174
- <h1>{this.applicationName}<span className='gs-sub-header'>{' ' + GettingStartedWidget.LABEL}</span></h1>
175
- </div>;
176
- }
177
-
178
- /**
179
- * Render the `Start` section.
180
- * Displays a collection of "start-to-work" related commands like `open` commands and some other.
181
- */
182
- protected renderStart(): React.ReactNode {
183
- const requireSingleOpen = isOSX || !environment.electron.is();
184
-
185
- const createFile = <div className='gs-action-container'>
186
- <a
187
- role={'button'}
188
- tabIndex={0}
189
- onClick={this.doCreateFile}
190
- onKeyDown={this.doCreateFileEnter}>
191
- {CommonCommands.NEW_UNTITLED_FILE.label ?? nls.localizeByDefault('New File...')}
192
- </a>
193
- </div>;
194
-
195
- const open = requireSingleOpen && <div className='gs-action-container'>
196
- <a
197
- role={'button'}
198
- tabIndex={0}
199
- onClick={this.doOpen}
200
- onKeyDown={this.doOpenEnter}>
201
- {nls.localizeByDefault('Open')}
202
- </a>
203
- </div>;
204
-
205
- const openFile = !requireSingleOpen && <div className='gs-action-container'>
206
- <a
207
- role={'button'}
208
- tabIndex={0}
209
- onClick={this.doOpenFile}
210
- onKeyDown={this.doOpenFileEnter}>
211
- {nls.localizeByDefault('Open File')}
212
- </a>
213
- </div>;
214
-
215
- const openFolder = !requireSingleOpen && <div className='gs-action-container'>
216
- <a
217
- role={'button'}
218
- tabIndex={0}
219
- onClick={this.doOpenFolder}
220
- onKeyDown={this.doOpenFolderEnter}>
221
- {nls.localizeByDefault('Open Folder')}
222
- </a>
223
- </div>;
224
-
225
- const openWorkspace = (
226
- <a
227
- role={'button'}
228
- tabIndex={0}
229
- onClick={this.doOpenWorkspace}
230
- onKeyDown={this.doOpenWorkspaceEnter}>
231
- {nls.localizeByDefault('Open Workspace')}
232
- </a>
233
- );
234
-
235
- return <div className='gs-section'>
236
- <h3 className='gs-section-header'><i className={codicon('folder-opened')}></i>{nls.localizeByDefault('Start')}</h3>
237
- {createFile}
238
- {open}
239
- {openFile}
240
- {openFolder}
241
- {openWorkspace}
242
- </div>;
243
- }
244
-
245
- /**
246
- * Render the recently used workspaces section.
247
- */
248
- protected renderRecentWorkspaces(): React.ReactNode {
249
- const items = this.recentWorkspaces;
250
- const paths = this.buildPaths(items);
251
- const content = paths.slice(0, this.recentLimit).map((item, index) =>
252
- <div className='gs-action-container' key={index}>
253
- <a
254
- role={'button'}
255
- tabIndex={0}
256
- onClick={() => this.open(new URI(items[index]))}
257
- onKeyDown={(e: React.KeyboardEvent) => this.openEnter(e, new URI(items[index]))}>
258
- {new URI(items[index]).path.base}
259
- </a>
260
- <span className='gs-action-details'>
261
- {item}
262
- </span>
263
- </div>
264
- );
265
- // If the recently used workspaces list exceeds the limit, display `More...` which triggers the recently used workspaces quick-open menu upon selection.
266
- const more = paths.length > this.recentLimit && <div className='gs-action-container'>
267
- <a
268
- role={'button'}
269
- tabIndex={0}
270
- onClick={this.doOpenRecentWorkspace}
271
- onKeyDown={this.doOpenRecentWorkspaceEnter}>
272
- {nls.localizeByDefault('More...')}
273
- </a>
274
- </div>;
275
- return <div className='gs-section'>
276
- <h3 className='gs-section-header'>
277
- <i className={codicon('history')}></i>{nls.localizeByDefault('Recent')}
278
- </h3>
279
- {items.length > 0 ? content : <p className='gs-no-recent'>
280
- {nls.localizeByDefault('You have no recent folders,') + ' '}
281
- <a
282
- role={'button'}
283
- tabIndex={0}
284
- onClick={this.doOpenFolder}
285
- onKeyDown={this.doOpenFolderEnter}>
286
- {nls.localizeByDefault('open a folder')}
287
- </a>
288
- {' ' + nls.localizeByDefault('to start.')}
289
- </p>}
290
- {more}
291
- </div>;
292
- }
293
-
294
- /**
295
- * Render the settings section.
296
- * Generally used to display useful links.
297
- */
298
- protected renderSettings(): React.ReactNode {
299
- return <div className='gs-section'>
300
- <h3 className='gs-section-header'>
301
- <i className={codicon('settings-gear')}></i>
302
- {nls.localizeByDefault('Settings')}
303
- </h3>
304
- <div className='gs-action-container'>
305
- <a
306
- role={'button'}
307
- tabIndex={0}
308
- onClick={this.doOpenPreferences}
309
- onKeyDown={this.doOpenPreferencesEnter}>
310
- {nls.localizeByDefault('Open Settings')}
311
- </a>
312
- </div>
313
- <div className='gs-action-container'>
314
- <a
315
- role={'button'}
316
- tabIndex={0}
317
- onClick={this.doOpenKeyboardShortcuts}
318
- onKeyDown={this.doOpenKeyboardShortcutsEnter}>
319
- {nls.localizeByDefault('Open Keyboard Shortcuts')}
320
- </a>
321
- </div>
322
- </div>;
323
- }
324
-
325
- /**
326
- * Render the help section.
327
- */
328
- protected renderHelp(): React.ReactNode {
329
- return <div className='gs-section'>
330
- <h3 className='gs-section-header'>
331
- <i className={codicon('question')}></i>
332
- {nls.localizeByDefault('Help')}
333
- </h3>
334
- <div className='gs-action-container'>
335
- <a
336
- role={'button'}
337
- tabIndex={0}
338
- onClick={() => this.doOpenExternalLink(this.documentationUrl)}
339
- onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.documentationUrl)}>
340
- {nls.localizeByDefault('Documentation')}
341
- </a>
342
- </div>
343
- <div className='gs-action-container'>
344
- <a
345
- role={'button'}
346
- tabIndex={0}
347
- onClick={() => this.doOpenExternalLink(this.compatibilityUrl)}
348
- onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.compatibilityUrl)}>
349
- {nls.localize('theia/getting-started/apiComparator', '{0} API Compatibility', 'VS Code')}
350
- </a>
351
- </div>
352
- <div className='gs-action-container'>
353
- <a
354
- role={'button'}
355
- tabIndex={0}
356
- onClick={() => this.doOpenExternalLink(this.extensionUrl)}
357
- onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.extensionUrl)}>
358
- {nls.localize('theia/getting-started/newExtension', 'Building a New Extension')}
359
- </a>
360
- </div>
361
- <div className='gs-action-container'>
362
- <a
363
- role={'button'}
364
- tabIndex={0}
365
- onClick={() => this.doOpenExternalLink(this.pluginUrl)}
366
- onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.pluginUrl)}>
367
- {nls.localize('theia/getting-started/newPlugin', 'Building a New Plugin')}
368
- </a>
369
- </div>
370
- </div>;
371
- }
372
-
373
- /**
374
- * Render the version section.
375
- */
376
- protected renderVersion(): React.ReactNode {
377
- return <div className='gs-section'>
378
- <div className='gs-action-container'>
379
- <p className='gs-sub-header' >
380
- {this.applicationInfo ? nls.localizeByDefault('Version: {0}', this.applicationInfo.version) : ''}
381
- </p>
382
- </div>
383
- </div>;
384
- }
385
-
386
- protected renderPreferences(): React.ReactNode {
387
- return <WelcomePreferences preferenceService={this.preferenceService}></WelcomePreferences>;
388
- }
389
-
390
- /**
391
- * Build the list of workspace paths.
392
- * @param workspaces {string[]} the list of workspaces.
393
- * @returns {string[]} the list of workspace paths.
394
- */
395
- protected buildPaths(workspaces: string[]): string[] {
396
- const paths: string[] = [];
397
- workspaces.forEach(workspace => {
398
- const uri = new URI(workspace);
399
- const pathLabel = this.labelProvider.getLongName(uri);
400
- const path = this.home ? Path.tildify(pathLabel, this.home) : pathLabel;
401
- paths.push(path);
402
- });
403
- return paths;
404
- }
405
-
406
- /**
407
- * Trigger the create file command.
408
- */
409
- protected doCreateFile = () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_FILE.id);
410
- protected doCreateFileEnter = (e: React.KeyboardEvent) => {
411
- if (this.isEnterKey(e)) {
412
- this.doCreateFile();
413
- }
414
- };
415
-
416
- /**
417
- * Trigger the open command.
418
- */
419
- protected doOpen = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN.id);
420
- protected doOpenEnter = (e: React.KeyboardEvent) => {
421
- if (this.isEnterKey(e)) {
422
- this.doOpen();
423
- }
424
- };
425
-
426
- /**
427
- * Trigger the open file command.
428
- */
429
- protected doOpenFile = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_FILE.id);
430
- protected doOpenFileEnter = (e: React.KeyboardEvent) => {
431
- if (this.isEnterKey(e)) {
432
- this.doOpenFile();
433
- }
434
- };
435
-
436
- /**
437
- * Trigger the open folder command.
438
- */
439
- protected doOpenFolder = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_FOLDER.id);
440
- protected doOpenFolderEnter = (e: React.KeyboardEvent) => {
441
- if (this.isEnterKey(e)) {
442
- this.doOpenFolder();
443
- }
444
- };
445
-
446
- /**
447
- * Trigger the open workspace command.
448
- */
449
- protected doOpenWorkspace = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_WORKSPACE.id);
450
- protected doOpenWorkspaceEnter = (e: React.KeyboardEvent) => {
451
- if (this.isEnterKey(e)) {
452
- this.doOpenWorkspace();
453
- }
454
- };
455
-
456
- /**
457
- * Trigger the open recent workspace command.
458
- */
459
- protected doOpenRecentWorkspace = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_RECENT_WORKSPACE.id);
460
- protected doOpenRecentWorkspaceEnter = (e: React.KeyboardEvent) => {
461
- if (this.isEnterKey(e)) {
462
- this.doOpenRecentWorkspace();
463
- }
464
- };
465
-
466
- /**
467
- * Trigger the open preferences command.
468
- * Used to open the preferences widget.
469
- */
470
- protected doOpenPreferences = () => this.commandRegistry.executeCommand(CommonCommands.OPEN_PREFERENCES.id);
471
- protected doOpenPreferencesEnter = (e: React.KeyboardEvent) => {
472
- if (this.isEnterKey(e)) {
473
- this.doOpenPreferences();
474
- }
475
- };
476
-
477
- /**
478
- * Trigger the open keyboard shortcuts command.
479
- * Used to open the keyboard shortcuts widget.
480
- */
481
- protected doOpenKeyboardShortcuts = () => this.commandRegistry.executeCommand(KeymapsCommands.OPEN_KEYMAPS.id);
482
- protected doOpenKeyboardShortcutsEnter = (e: React.KeyboardEvent) => {
483
- if (this.isEnterKey(e)) {
484
- this.doOpenKeyboardShortcuts();
485
- }
486
- };
487
-
488
- /**
489
- * Open a workspace given its uri.
490
- * @param uri {URI} the workspace uri.
491
- */
492
- protected open = (uri: URI) => this.workspaceService.open(uri);
493
- protected openEnter = (e: React.KeyboardEvent, uri: URI) => {
494
- if (this.isEnterKey(e)) {
495
- this.open(uri);
496
- }
497
- };
498
-
499
- /**
500
- * Open a link in an external window.
501
- * @param url the link.
502
- */
503
- protected doOpenExternalLink = (url: string) => this.windowService.openNewWindow(url, { external: true });
504
- protected doOpenExternalLinkEnter = (e: React.KeyboardEvent, url: string) => {
505
- if (this.isEnterKey(e)) {
506
- this.doOpenExternalLink(url);
507
- }
508
- };
509
-
510
- protected isEnterKey(e: React.KeyboardEvent): boolean {
511
- return Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
512
- }
513
- }
514
-
515
- export interface PreferencesProps {
516
- preferenceService: PreferenceService;
517
- }
518
-
519
- function WelcomePreferences(props: PreferencesProps): JSX.Element {
520
- const [startupEditor, setStartupEditor] = React.useState<string>(
521
- props.preferenceService.get('workbench.startupEditor', 'welcomePage')
522
- );
523
- React.useEffect(() => {
524
- const prefListener = props.preferenceService.onPreferenceChanged(change => {
525
- if (change.preferenceName === 'workbench.startupEditor') {
526
- const prefValue = change.newValue;
527
- setStartupEditor(prefValue);
528
- }
529
- });
530
- return () => prefListener.dispose();
531
- }, [props.preferenceService]);
532
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
533
- const newValue = e.target.checked ? 'welcomePage' : 'none';
534
- props.preferenceService.updateValue('workbench.startupEditor', newValue);
535
- };
536
- return (
537
- <div className='gs-preference'>
538
- <input
539
- type="checkbox"
540
- className="theia-input"
541
- id="startupEditor"
542
- onChange={handleChange}
543
- checked={startupEditor === 'welcomePage' || startupEditor === 'welcomePageInEmptyWorkbench'}
544
- />
545
- <label htmlFor="startupEditor">
546
- {nls.localizeByDefault('Show welcome page on startup')}
547
- </label>
548
- </div>
549
- );
550
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2018 Ericsson and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { codicon, CommonCommands, Key, KeyCode, LabelProvider, Message, PreferenceService, ReactWidget } from '@theia/core/lib/browser';
18
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
19
+ import { WindowService } from '@theia/core/lib/browser/window/window-service';
20
+ import { CommandRegistry, environment, isOSX, Path } from '@theia/core/lib/common';
21
+ import { ApplicationInfo, ApplicationServer } from '@theia/core/lib/common/application-protocol';
22
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
23
+ import { nls } from '@theia/core/lib/common/nls';
24
+ import URI from '@theia/core/lib/common/uri';
25
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
26
+ import * as React from '@theia/core/shared/react';
27
+ import { KeymapsCommands } from '@theia/keymaps/lib/browser';
28
+ import { WorkspaceCommands, WorkspaceService } from '@theia/workspace/lib/browser';
29
+
30
+ /**
31
+ * Default implementation of the `GettingStartedWidget`.
32
+ * The widget is displayed when there are currently no workspaces present.
33
+ * Some of the features displayed include:
34
+ * - `open` commands.
35
+ * - `recently used workspaces`.
36
+ * - `settings` commands.
37
+ * - `help` commands.
38
+ * - helpful links.
39
+ */
40
+ @injectable()
41
+ export class GettingStartedWidget extends ReactWidget {
42
+
43
+ /**
44
+ * The widget `id`.
45
+ */
46
+ static readonly ID = 'getting.started.widget';
47
+ /**
48
+ * The widget `label` which is used for display purposes.
49
+ */
50
+ static readonly LABEL = nls.localizeByDefault('Welcome');
51
+
52
+ /**
53
+ * The `ApplicationInfo` for the application if available.
54
+ * Used in order to obtain the version number of the application.
55
+ */
56
+ protected applicationInfo: ApplicationInfo | undefined;
57
+ /**
58
+ * The application name which is used for display purposes.
59
+ */
60
+ protected applicationName = FrontendApplicationConfigProvider.get().applicationName;
61
+
62
+ protected home: string | undefined;
63
+
64
+ /**
65
+ * The recently used workspaces limit.
66
+ * Used in order to limit the number of recently used workspaces to display.
67
+ */
68
+ protected recentLimit = 5;
69
+ /**
70
+ * The list of recently used workspaces.
71
+ */
72
+ protected recentWorkspaces: string[] = [];
73
+
74
+ /**
75
+ * Indicates whether the "ai-core" extension is available.
76
+ */
77
+ protected aiIsIncluded: boolean;
78
+
79
+ /**
80
+ * Collection of useful links to display for end users.
81
+ */
82
+ protected readonly documentationUrl = 'https://www.theia-ide.org/docs/';
83
+ protected readonly compatibilityUrl = 'https://eclipse-theia.github.io/vscode-theia-comparator/status.html';
84
+ protected readonly extensionUrl = 'https://www.theia-ide.org/docs/authoring_extensions';
85
+ protected readonly pluginUrl = 'https://www.theia-ide.org/docs/authoring_plugins';
86
+ protected readonly theiaAIDocUrl = 'https://theia-ide.org/docs/user_ai/';
87
+ protected readonly ghProjectUrl = 'https://github.com/eclipse-theia/theia/issues/new/choose';
88
+
89
+ @inject(ApplicationServer)
90
+ protected readonly appServer: ApplicationServer;
91
+
92
+ @inject(CommandRegistry)
93
+ protected readonly commandRegistry: CommandRegistry;
94
+
95
+ @inject(EnvVariablesServer)
96
+ protected readonly environments: EnvVariablesServer;
97
+
98
+ @inject(LabelProvider)
99
+ protected readonly labelProvider: LabelProvider;
100
+
101
+ @inject(WindowService)
102
+ protected readonly windowService: WindowService;
103
+
104
+ @inject(WorkspaceService)
105
+ protected readonly workspaceService: WorkspaceService;
106
+
107
+ @inject(PreferenceService)
108
+ protected readonly preferenceService: PreferenceService;
109
+
110
+ @postConstruct()
111
+ protected init(): void {
112
+ this.doInit();
113
+ }
114
+
115
+ protected async doInit(): Promise<void> {
116
+ this.id = GettingStartedWidget.ID;
117
+ this.title.label = GettingStartedWidget.LABEL;
118
+ this.title.caption = GettingStartedWidget.LABEL;
119
+ this.title.closable = true;
120
+
121
+ this.applicationInfo = await this.appServer.getApplicationInfo();
122
+ this.recentWorkspaces = await this.workspaceService.recentWorkspaces();
123
+ this.home = new URI(await this.environments.getHomeDirUri()).path.toString();
124
+
125
+ const extensions = await this.appServer.getExtensionsInfos();
126
+ this.aiIsIncluded = extensions.find(ext => ext.name === '@theia/ai-core') !== undefined;
127
+ this.update();
128
+ }
129
+
130
+ protected override onActivateRequest(msg: Message): void {
131
+ super.onActivateRequest(msg);
132
+ const elArr = this.node.getElementsByTagName('a');
133
+ if (elArr && elArr.length > 0) {
134
+ (elArr[0] as HTMLElement).focus();
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Render the content of the widget.
140
+ */
141
+ protected render(): React.ReactNode {
142
+ return <div className='gs-container'>
143
+ <div className='gs-content-container'>
144
+ {this.aiIsIncluded &&
145
+ <div className='gs-float shadow-pulse'>
146
+ {this.renderAIBanner()}
147
+ </div>
148
+ }
149
+ {this.renderHeader()}
150
+ <hr className='gs-hr' />
151
+ <div className='flex-grid'>
152
+ <div className='col'>
153
+ {this.renderStart()}
154
+ </div>
155
+ </div>
156
+ <div className='flex-grid'>
157
+ <div className='col'>
158
+ {this.renderRecentWorkspaces()}
159
+ </div>
160
+ </div>
161
+ <div className='flex-grid'>
162
+ <div className='col'>
163
+ {this.renderSettings()}
164
+ </div>
165
+ </div>
166
+ <div className='flex-grid'>
167
+ <div className='col'>
168
+ {this.renderHelp()}
169
+ </div>
170
+ </div>
171
+ <div className='flex-grid'>
172
+ <div className='col'>
173
+ {this.renderVersion()}
174
+ </div>
175
+ </div>
176
+ </div>
177
+ <div className='gs-preference-container'>
178
+ {this.renderPreferences()}
179
+ </div>
180
+ </div>;
181
+ }
182
+
183
+ /**
184
+ * Render the widget header.
185
+ * Renders the title `{applicationName} Getting Started`.
186
+ */
187
+ protected renderHeader(): React.ReactNode {
188
+ return <div className='gs-header'>
189
+ <h1>{this.applicationName}<span className='gs-sub-header'>{' ' + GettingStartedWidget.LABEL}</span></h1>
190
+ </div>;
191
+ }
192
+
193
+ /**
194
+ * Render the `Start` section.
195
+ * Displays a collection of "start-to-work" related commands like `open` commands and some other.
196
+ */
197
+ protected renderStart(): React.ReactNode {
198
+ const requireSingleOpen = isOSX || !environment.electron.is();
199
+
200
+ const createFile = <div className='gs-action-container'>
201
+ <a
202
+ role={'button'}
203
+ tabIndex={0}
204
+ onClick={this.doCreateFile}
205
+ onKeyDown={this.doCreateFileEnter}>
206
+ {CommonCommands.NEW_UNTITLED_FILE.label ?? nls.localizeByDefault('New File...')}
207
+ </a>
208
+ </div>;
209
+
210
+ const open = requireSingleOpen && <div className='gs-action-container'>
211
+ <a
212
+ role={'button'}
213
+ tabIndex={0}
214
+ onClick={this.doOpen}
215
+ onKeyDown={this.doOpenEnter}>
216
+ {nls.localizeByDefault('Open')}
217
+ </a>
218
+ </div>;
219
+
220
+ const openFile = !requireSingleOpen && <div className='gs-action-container'>
221
+ <a
222
+ role={'button'}
223
+ tabIndex={0}
224
+ onClick={this.doOpenFile}
225
+ onKeyDown={this.doOpenFileEnter}>
226
+ {nls.localizeByDefault('Open File')}
227
+ </a>
228
+ </div>;
229
+
230
+ const openFolder = !requireSingleOpen && <div className='gs-action-container'>
231
+ <a
232
+ role={'button'}
233
+ tabIndex={0}
234
+ onClick={this.doOpenFolder}
235
+ onKeyDown={this.doOpenFolderEnter}>
236
+ {nls.localizeByDefault('Open Folder')}
237
+ </a>
238
+ </div>;
239
+
240
+ const openWorkspace = (
241
+ <a
242
+ role={'button'}
243
+ tabIndex={0}
244
+ onClick={this.doOpenWorkspace}
245
+ onKeyDown={this.doOpenWorkspaceEnter}>
246
+ {nls.localizeByDefault('Open Workspace')}
247
+ </a>
248
+ );
249
+
250
+ return <div className='gs-section'>
251
+ <h3 className='gs-section-header'><i className={codicon('folder-opened')}></i>{nls.localizeByDefault('Start')}</h3>
252
+ {createFile}
253
+ {open}
254
+ {openFile}
255
+ {openFolder}
256
+ {openWorkspace}
257
+ </div>;
258
+ }
259
+
260
+ /**
261
+ * Render the recently used workspaces section.
262
+ */
263
+ protected renderRecentWorkspaces(): React.ReactNode {
264
+ const items = this.recentWorkspaces;
265
+ const paths = this.buildPaths(items);
266
+ const content = paths.slice(0, this.recentLimit).map((item, index) =>
267
+ <div className='gs-action-container' key={index}>
268
+ <a
269
+ role={'button'}
270
+ tabIndex={0}
271
+ onClick={() => this.open(new URI(items[index]))}
272
+ onKeyDown={(e: React.KeyboardEvent) => this.openEnter(e, new URI(items[index]))}>
273
+ {new URI(items[index]).path.base}
274
+ </a>
275
+ <span className='gs-action-details'>
276
+ {item}
277
+ </span>
278
+ </div>
279
+ );
280
+ // If the recently used workspaces list exceeds the limit, display `More...` which triggers the recently used workspaces quick-open menu upon selection.
281
+ const more = paths.length > this.recentLimit && <div className='gs-action-container'>
282
+ <a
283
+ role={'button'}
284
+ tabIndex={0}
285
+ onClick={this.doOpenRecentWorkspace}
286
+ onKeyDown={this.doOpenRecentWorkspaceEnter}>
287
+ {nls.localizeByDefault('More...')}
288
+ </a>
289
+ </div>;
290
+ return <div className='gs-section'>
291
+ <h3 className='gs-section-header'>
292
+ <i className={codicon('history')}></i>{nls.localizeByDefault('Recent')}
293
+ </h3>
294
+ {items.length > 0 ? content : <p className='gs-no-recent'>
295
+ {nls.localizeByDefault('You have no recent folders,') + ' '}
296
+ <a
297
+ role={'button'}
298
+ tabIndex={0}
299
+ onClick={this.doOpenFolder}
300
+ onKeyDown={this.doOpenFolderEnter}>
301
+ {nls.localizeByDefault('open a folder')}
302
+ </a>
303
+ {' ' + nls.localizeByDefault('to start.')}
304
+ </p>}
305
+ {more}
306
+ </div>;
307
+ }
308
+
309
+ /**
310
+ * Render the settings section.
311
+ * Generally used to display useful links.
312
+ */
313
+ protected renderSettings(): React.ReactNode {
314
+ return <div className='gs-section'>
315
+ <h3 className='gs-section-header'>
316
+ <i className={codicon('settings-gear')}></i>
317
+ {nls.localizeByDefault('Settings')}
318
+ </h3>
319
+ <div className='gs-action-container'>
320
+ <a
321
+ role={'button'}
322
+ tabIndex={0}
323
+ onClick={this.doOpenPreferences}
324
+ onKeyDown={this.doOpenPreferencesEnter}>
325
+ {nls.localizeByDefault('Open Settings')}
326
+ </a>
327
+ </div>
328
+ <div className='gs-action-container'>
329
+ <a
330
+ role={'button'}
331
+ tabIndex={0}
332
+ onClick={this.doOpenKeyboardShortcuts}
333
+ onKeyDown={this.doOpenKeyboardShortcutsEnter}>
334
+ {nls.localizeByDefault('Open Keyboard Shortcuts')}
335
+ </a>
336
+ </div>
337
+ </div>;
338
+ }
339
+
340
+ /**
341
+ * Render the help section.
342
+ */
343
+ protected renderHelp(): React.ReactNode {
344
+ return <div className='gs-section'>
345
+ <h3 className='gs-section-header'>
346
+ <i className={codicon('question')}></i>
347
+ {nls.localizeByDefault('Help')}
348
+ </h3>
349
+ <div className='gs-action-container'>
350
+ <a
351
+ role={'button'}
352
+ tabIndex={0}
353
+ onClick={() => this.doOpenExternalLink(this.documentationUrl)}
354
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.documentationUrl)}>
355
+ {nls.localizeByDefault('Documentation')}
356
+ </a>
357
+ </div>
358
+ <div className='gs-action-container'>
359
+ <a
360
+ role={'button'}
361
+ tabIndex={0}
362
+ onClick={() => this.doOpenExternalLink(this.compatibilityUrl)}
363
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.compatibilityUrl)}>
364
+ {nls.localize('theia/getting-started/apiComparator', '{0} API Compatibility', 'VS Code')}
365
+ </a>
366
+ </div>
367
+ <div className='gs-action-container'>
368
+ <a
369
+ role={'button'}
370
+ tabIndex={0}
371
+ onClick={() => this.doOpenExternalLink(this.extensionUrl)}
372
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.extensionUrl)}>
373
+ {nls.localize('theia/getting-started/newExtension', 'Building a New Extension')}
374
+ </a>
375
+ </div>
376
+ <div className='gs-action-container'>
377
+ <a
378
+ role={'button'}
379
+ tabIndex={0}
380
+ onClick={() => this.doOpenExternalLink(this.pluginUrl)}
381
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.pluginUrl)}>
382
+ {nls.localize('theia/getting-started/newPlugin', 'Building a New Plugin')}
383
+ </a>
384
+ </div>
385
+ </div>;
386
+ }
387
+
388
+ /**
389
+ * Render the version section.
390
+ */
391
+ protected renderVersion(): React.ReactNode {
392
+ return <div className='gs-section'>
393
+ <div className='gs-action-container'>
394
+ <p className='gs-sub-header' >
395
+ {this.applicationInfo ? nls.localizeByDefault('Version: {0}', this.applicationInfo.version) : ''}
396
+ </p>
397
+ </div>
398
+ </div>;
399
+ }
400
+
401
+ protected renderPreferences(): React.ReactNode {
402
+ return <WelcomePreferences preferenceService={this.preferenceService}></WelcomePreferences>;
403
+ }
404
+
405
+ protected renderAIBanner(): React.ReactNode {
406
+ return <div className='gs-container gs-experimental-container'>
407
+ <div className='flex-grid'>
408
+ <div className='col'>
409
+ <h3 className='gs-section-header'> 🚀 Theia AI [Experimental] is available! ✨</h3>
410
+ <br />
411
+ <div className='gs-action-container'>
412
+ Theia IDE now contains the experimental "Theia AI" feature, which offers early access to cutting-edge AI capabilities within your IDE.
413
+ <br />
414
+ <br />
415
+ Please note that these features are disabled by default, ensuring that users can opt-in at their discretion without any concerns.
416
+ For those who choose to enable Theia AI, it is important to be aware that these experimental features may generate continuous
417
+ requests to the language models (LLMs) you provide access to, potentially incurring additional costs.
418
+ <br />
419
+ For more details, please visit &nbsp;
420
+ <a
421
+ role={'button'}
422
+ tabIndex={0}
423
+ onClick={() => this.doOpenExternalLink(this.theiaAIDocUrl)}
424
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.theiaAIDocUrl)}>
425
+ {'Theia AI Documentation'}
426
+ </a>.
427
+ <br />
428
+ <br />
429
+ We encourage feedback, contributions, and sponsorship to support the ongoing development of the Theia AI initiative use our&nbsp;
430
+ <a
431
+ role={'button'}
432
+ tabIndex={0}
433
+ onClick={() => this.doOpenExternalLink(this.ghProjectUrl)}
434
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenExternalLinkEnter(e, this.ghProjectUrl)}>
435
+ {'Github Project'}
436
+ </a>.
437
+ &nbsp;Thank you for being part of our community!
438
+ <br />
439
+ <br />
440
+ Please note that this feature is currently in development and may undergo frequent changes. 🚧
441
+ </div>
442
+ <br />
443
+ <div className='gs-action-container'>
444
+ Let's dive in!<br /><br />
445
+ <a
446
+ role={'button'}
447
+ style={{ fontSize: 'var(--theia-ui-font-size2)' }}
448
+ tabIndex={0}
449
+ onClick={() => this.doOpenAIChatView()}
450
+ onKeyDown={(e: React.KeyboardEvent) => this.doOpenAIChatViewEnter(e)}>
451
+ {'Open the Theia AI Chat View now to catch a first glimpse and learn how to begin! ✨'}
452
+ </a>
453
+ </div>
454
+ <br />
455
+ <br />
456
+ </div>
457
+ </div>
458
+ </div>;
459
+ }
460
+
461
+ protected doOpenAIChatView = () => this.commandRegistry.executeCommand('ai-chat:open');
462
+ protected doOpenAIChatViewEnter = (e: React.KeyboardEvent) => {
463
+ if (this.isEnterKey(e)) {
464
+ this.doOpenAIChatView();
465
+ }
466
+ };
467
+
468
+ /**
469
+ * Build the list of workspace paths.
470
+ * @param workspaces {string[]} the list of workspaces.
471
+ * @returns {string[]} the list of workspace paths.
472
+ */
473
+ protected buildPaths(workspaces: string[]): string[] {
474
+ const paths: string[] = [];
475
+ workspaces.forEach(workspace => {
476
+ const uri = new URI(workspace);
477
+ const pathLabel = this.labelProvider.getLongName(uri);
478
+ const path = this.home ? Path.tildify(pathLabel, this.home) : pathLabel;
479
+ paths.push(path);
480
+ });
481
+ return paths;
482
+ }
483
+
484
+ /**
485
+ * Trigger the create file command.
486
+ */
487
+ protected doCreateFile = () => this.commandRegistry.executeCommand(CommonCommands.NEW_UNTITLED_FILE.id);
488
+ protected doCreateFileEnter = (e: React.KeyboardEvent) => {
489
+ if (this.isEnterKey(e)) {
490
+ this.doCreateFile();
491
+ }
492
+ };
493
+
494
+ /**
495
+ * Trigger the open command.
496
+ */
497
+ protected doOpen = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN.id);
498
+ protected doOpenEnter = (e: React.KeyboardEvent) => {
499
+ if (this.isEnterKey(e)) {
500
+ this.doOpen();
501
+ }
502
+ };
503
+
504
+ /**
505
+ * Trigger the open file command.
506
+ */
507
+ protected doOpenFile = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_FILE.id);
508
+ protected doOpenFileEnter = (e: React.KeyboardEvent) => {
509
+ if (this.isEnterKey(e)) {
510
+ this.doOpenFile();
511
+ }
512
+ };
513
+
514
+ /**
515
+ * Trigger the open folder command.
516
+ */
517
+ protected doOpenFolder = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_FOLDER.id);
518
+ protected doOpenFolderEnter = (e: React.KeyboardEvent) => {
519
+ if (this.isEnterKey(e)) {
520
+ this.doOpenFolder();
521
+ }
522
+ };
523
+
524
+ /**
525
+ * Trigger the open workspace command.
526
+ */
527
+ protected doOpenWorkspace = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_WORKSPACE.id);
528
+ protected doOpenWorkspaceEnter = (e: React.KeyboardEvent) => {
529
+ if (this.isEnterKey(e)) {
530
+ this.doOpenWorkspace();
531
+ }
532
+ };
533
+
534
+ /**
535
+ * Trigger the open recent workspace command.
536
+ */
537
+ protected doOpenRecentWorkspace = () => this.commandRegistry.executeCommand(WorkspaceCommands.OPEN_RECENT_WORKSPACE.id);
538
+ protected doOpenRecentWorkspaceEnter = (e: React.KeyboardEvent) => {
539
+ if (this.isEnterKey(e)) {
540
+ this.doOpenRecentWorkspace();
541
+ }
542
+ };
543
+
544
+ /**
545
+ * Trigger the open preferences command.
546
+ * Used to open the preferences widget.
547
+ */
548
+ protected doOpenPreferences = () => this.commandRegistry.executeCommand(CommonCommands.OPEN_PREFERENCES.id);
549
+ protected doOpenPreferencesEnter = (e: React.KeyboardEvent) => {
550
+ if (this.isEnterKey(e)) {
551
+ this.doOpenPreferences();
552
+ }
553
+ };
554
+
555
+ /**
556
+ * Trigger the open keyboard shortcuts command.
557
+ * Used to open the keyboard shortcuts widget.
558
+ */
559
+ protected doOpenKeyboardShortcuts = () => this.commandRegistry.executeCommand(KeymapsCommands.OPEN_KEYMAPS.id);
560
+ protected doOpenKeyboardShortcutsEnter = (e: React.KeyboardEvent) => {
561
+ if (this.isEnterKey(e)) {
562
+ this.doOpenKeyboardShortcuts();
563
+ }
564
+ };
565
+
566
+ /**
567
+ * Open a workspace given its uri.
568
+ * @param uri {URI} the workspace uri.
569
+ */
570
+ protected open = (uri: URI) => this.workspaceService.open(uri);
571
+ protected openEnter = (e: React.KeyboardEvent, uri: URI) => {
572
+ if (this.isEnterKey(e)) {
573
+ this.open(uri);
574
+ }
575
+ };
576
+
577
+ /**
578
+ * Open a link in an external window.
579
+ * @param url the link.
580
+ */
581
+ protected doOpenExternalLink = (url: string) => this.windowService.openNewWindow(url, { external: true });
582
+ protected doOpenExternalLinkEnter = (e: React.KeyboardEvent, url: string) => {
583
+ if (this.isEnterKey(e)) {
584
+ this.doOpenExternalLink(url);
585
+ }
586
+ };
587
+
588
+ protected isEnterKey(e: React.KeyboardEvent): boolean {
589
+ return Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
590
+ }
591
+ }
592
+
593
+ export interface PreferencesProps {
594
+ preferenceService: PreferenceService;
595
+ }
596
+
597
+ function WelcomePreferences(props: PreferencesProps): JSX.Element {
598
+ const [startupEditor, setStartupEditor] = React.useState<string>(
599
+ props.preferenceService.get('workbench.startupEditor', 'welcomePage')
600
+ );
601
+ React.useEffect(() => {
602
+ const prefListener = props.preferenceService.onPreferenceChanged(change => {
603
+ if (change.preferenceName === 'workbench.startupEditor') {
604
+ const prefValue = change.newValue;
605
+ setStartupEditor(prefValue);
606
+ }
607
+ });
608
+ return () => prefListener.dispose();
609
+ }, [props.preferenceService]);
610
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
611
+ const newValue = e.target.checked ? 'welcomePage' : 'none';
612
+ props.preferenceService.updateValue('workbench.startupEditor', newValue);
613
+ };
614
+ return (
615
+ <div className='gs-preference'>
616
+ <input
617
+ type="checkbox"
618
+ className="theia-input"
619
+ id="startupEditor"
620
+ onChange={handleChange}
621
+ checked={startupEditor === 'welcomePage' || startupEditor === 'welcomePageInEmptyWorkbench'}
622
+ />
623
+ <label htmlFor="startupEditor">
624
+ {nls.localizeByDefault('Show welcome page on startup')}
625
+ </label>
626
+ </div>
627
+ );
628
+ }