@theia/core 1.40.1 → 1.41.0

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.
Files changed (167) hide show
  1. package/README.md +6 -6
  2. package/i18n/nls.cs.json +5 -1
  3. package/i18n/nls.de.json +5 -1
  4. package/i18n/nls.es.json +5 -1
  5. package/i18n/nls.fr.json +5 -1
  6. package/i18n/nls.hu.json +5 -1
  7. package/i18n/nls.it.json +5 -1
  8. package/i18n/nls.ja.json +5 -1
  9. package/i18n/nls.json +5 -1
  10. package/i18n/nls.pl.json +5 -1
  11. package/i18n/nls.pt-br.json +5 -1
  12. package/i18n/nls.pt-pt.json +5 -1
  13. package/i18n/nls.ru.json +5 -1
  14. package/i18n/nls.zh-cn.json +5 -1
  15. package/lib/browser/common-frontend-contribution.d.ts +11 -0
  16. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  17. package/lib/browser/common-frontend-contribution.js +97 -14
  18. package/lib/browser/common-frontend-contribution.js.map +1 -1
  19. package/lib/browser/context-menu-renderer.d.ts +5 -0
  20. package/lib/browser/context-menu-renderer.d.ts.map +1 -1
  21. package/lib/browser/label-parser.d.ts +8 -0
  22. package/lib/browser/label-parser.d.ts.map +1 -1
  23. package/lib/browser/label-parser.js +14 -0
  24. package/lib/browser/label-parser.js.map +1 -1
  25. package/lib/browser/label-parser.spec.js +33 -0
  26. package/lib/browser/label-parser.spec.js.map +1 -1
  27. package/lib/browser/menu/browser-context-menu-renderer.d.ts +1 -1
  28. package/lib/browser/menu/browser-context-menu-renderer.d.ts.map +1 -1
  29. package/lib/browser/menu/browser-context-menu-renderer.js +2 -2
  30. package/lib/browser/menu/browser-context-menu-renderer.js.map +1 -1
  31. package/lib/browser/menu/browser-menu-plugin.d.ts +1 -1
  32. package/lib/browser/menu/browser-menu-plugin.d.ts.map +1 -1
  33. package/lib/browser/menu/browser-menu-plugin.js +2 -2
  34. package/lib/browser/menu/browser-menu-plugin.js.map +1 -1
  35. package/lib/browser/performance/frontend-stopwatch.d.ts.map +1 -1
  36. package/lib/browser/performance/frontend-stopwatch.js +6 -2
  37. package/lib/browser/performance/frontend-stopwatch.js.map +1 -1
  38. package/lib/browser/preferences/preference-contribution.d.ts +2 -0
  39. package/lib/browser/preferences/preference-contribution.d.ts.map +1 -1
  40. package/lib/browser/preferences/preference-contribution.js +48 -24
  41. package/lib/browser/preferences/preference-contribution.js.map +1 -1
  42. package/lib/browser/saveable.d.ts +15 -1
  43. package/lib/browser/saveable.d.ts.map +1 -1
  44. package/lib/browser/saveable.js +34 -1
  45. package/lib/browser/saveable.js.map +1 -1
  46. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.d.ts +46 -1
  47. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.d.ts.map +1 -1
  48. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.js +87 -6
  49. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.js.map +1 -1
  50. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.d.ts +20 -2
  51. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.d.ts.map +1 -1
  52. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.js +28 -1
  53. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.js.map +1 -1
  54. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts +25 -1
  55. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts.map +1 -1
  56. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js +79 -7
  57. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js.map +1 -1
  58. package/lib/browser/shell/tab-bars.d.ts.map +1 -1
  59. package/lib/browser/shell/tab-bars.js +9 -1
  60. package/lib/browser/shell/tab-bars.js.map +1 -1
  61. package/lib/browser/tree/tree-model.d.ts +2 -0
  62. package/lib/browser/tree/tree-model.d.ts.map +1 -1
  63. package/lib/browser/tree/tree-model.js +6 -0
  64. package/lib/browser/tree/tree-model.js.map +1 -1
  65. package/lib/browser/tree/tree-widget.d.ts +18 -0
  66. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  67. package/lib/browser/tree/tree-widget.js +57 -0
  68. package/lib/browser/tree/tree-widget.js.map +1 -1
  69. package/lib/browser/tree/tree.d.ts +18 -0
  70. package/lib/browser/tree/tree.d.ts.map +1 -1
  71. package/lib/browser/tree/tree.js +6 -0
  72. package/lib/browser/tree/tree.js.map +1 -1
  73. package/lib/browser/widgets/enhanced-preview-widget.d.ts +7 -0
  74. package/lib/browser/widgets/enhanced-preview-widget.d.ts.map +1 -0
  75. package/lib/browser/widgets/enhanced-preview-widget.js +27 -0
  76. package/lib/browser/widgets/enhanced-preview-widget.js.map +1 -0
  77. package/lib/browser/window-contribution.js +1 -1
  78. package/lib/browser/window-contribution.js.map +1 -1
  79. package/lib/common/array-utils.d.ts +7 -0
  80. package/lib/common/array-utils.d.ts.map +1 -1
  81. package/lib/common/array-utils.js +21 -0
  82. package/lib/common/array-utils.js.map +1 -1
  83. package/lib/common/event.d.ts +5 -0
  84. package/lib/common/event.d.ts.map +1 -1
  85. package/lib/common/event.js +5 -1
  86. package/lib/common/event.js.map +1 -1
  87. package/lib/common/menu/menu-model-registry.d.ts +12 -1
  88. package/lib/common/menu/menu-model-registry.d.ts.map +1 -1
  89. package/lib/common/menu/menu-model-registry.js +46 -0
  90. package/lib/common/menu/menu-model-registry.js.map +1 -1
  91. package/lib/common/performance/measurement.d.ts +21 -0
  92. package/lib/common/performance/measurement.d.ts.map +1 -1
  93. package/lib/common/performance/stopwatch.d.ts +10 -2
  94. package/lib/common/performance/stopwatch.d.ts.map +1 -1
  95. package/lib/common/performance/stopwatch.js +34 -11
  96. package/lib/common/performance/stopwatch.js.map +1 -1
  97. package/lib/common/promise-util.d.ts +4 -0
  98. package/lib/common/promise-util.d.ts.map +1 -1
  99. package/lib/common/promise-util.js +11 -1
  100. package/lib/common/promise-util.js.map +1 -1
  101. package/lib/common/promise-util.spec.js +26 -12
  102. package/lib/common/promise-util.spec.js.map +1 -1
  103. package/lib/common/types.d.ts +4 -0
  104. package/lib/common/types.d.ts.map +1 -1
  105. package/lib/common/types.js +16 -1
  106. package/lib/common/types.js.map +1 -1
  107. package/lib/common/uri.d.ts +1 -0
  108. package/lib/common/uri.d.ts.map +1 -1
  109. package/lib/common/uri.js +3 -0
  110. package/lib/common/uri.js.map +1 -1
  111. package/lib/electron-browser/menu/electron-context-menu-renderer.js +2 -2
  112. package/lib/electron-browser/menu/electron-context-menu-renderer.js.map +1 -1
  113. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts +1 -1
  114. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  115. package/lib/electron-browser/menu/electron-main-menu-factory.js +2 -2
  116. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  117. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  118. package/lib/electron-main/electron-main-application.js +3 -0
  119. package/lib/electron-main/electron-main-application.js.map +1 -1
  120. package/lib/node/backend-application.d.ts +5 -1
  121. package/lib/node/backend-application.d.ts.map +1 -1
  122. package/lib/node/backend-application.js +28 -5
  123. package/lib/node/backend-application.js.map +1 -1
  124. package/lib/node/logger-cli-contribution.spec.js +1 -1
  125. package/lib/node/logger-cli-contribution.spec.js.map +1 -1
  126. package/lib/node/main.d.ts +2 -5
  127. package/lib/node/main.d.ts.map +1 -1
  128. package/lib/node/main.js.map +1 -1
  129. package/lib/node/performance/node-stopwatch.js +1 -1
  130. package/lib/node/performance/node-stopwatch.js.map +1 -1
  131. package/package.json +7 -7
  132. package/src/browser/common-frontend-contribution.ts +107 -17
  133. package/src/browser/context-menu-renderer.ts +5 -0
  134. package/src/browser/label-parser.spec.ts +38 -0
  135. package/src/browser/label-parser.ts +15 -0
  136. package/src/browser/menu/browser-context-menu-renderer.ts +2 -2
  137. package/src/browser/menu/browser-menu-plugin.ts +2 -2
  138. package/src/browser/performance/frontend-stopwatch.ts +5 -2
  139. package/src/browser/preferences/preference-contribution.ts +49 -24
  140. package/src/browser/saveable.ts +41 -2
  141. package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts +94 -8
  142. package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts +28 -1
  143. package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +87 -8
  144. package/src/browser/shell/tab-bars.ts +8 -1
  145. package/src/browser/style/tabs.css +32 -2
  146. package/src/browser/tree/tree-model.ts +8 -0
  147. package/src/browser/tree/tree-widget.tsx +66 -0
  148. package/src/browser/tree/tree.ts +27 -0
  149. package/src/browser/widgets/enhanced-preview-widget.ts +27 -0
  150. package/src/browser/window-contribution.ts +1 -1
  151. package/src/common/array-utils.ts +20 -0
  152. package/src/common/event.ts +12 -2
  153. package/src/common/i18n/nls.metadata.json +5616 -5359
  154. package/src/common/menu/menu-model-registry.ts +50 -0
  155. package/src/common/performance/measurement.ts +26 -0
  156. package/src/common/performance/stopwatch.ts +38 -12
  157. package/src/common/promise-util.spec.ts +43 -12
  158. package/src/common/promise-util.ts +12 -0
  159. package/src/common/types.ts +17 -0
  160. package/src/common/uri.ts +4 -0
  161. package/src/electron-browser/menu/electron-context-menu-renderer.ts +2 -2
  162. package/src/electron-browser/menu/electron-main-menu-factory.ts +2 -2
  163. package/src/electron-main/electron-main-application.ts +3 -0
  164. package/src/node/backend-application.ts +30 -5
  165. package/src/node/logger-cli-contribution.spec.ts +1 -1
  166. package/src/node/main.ts +1 -6
  167. package/src/node/performance/node-stopwatch.ts +1 -1
@@ -17,11 +17,11 @@
17
17
  import debounce = require('lodash.debounce');
18
18
  import { inject, injectable, named } from 'inversify';
19
19
  // eslint-disable-next-line max-len
20
- import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuPath } from '../../../common';
20
+ import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common';
21
21
  import { ContextKeyService } from '../../context-key-service';
22
22
  import { FrontendApplicationContribution } from '../../frontend-application';
23
23
  import { Widget } from '../../widgets';
24
- import { MenuDelegate, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types';
24
+ import { AnyToolbarItem, ConditionalToolbarItem, MenuDelegate, MenuToolbarItem, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types';
25
25
  import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters';
26
26
 
27
27
  /**
@@ -103,10 +103,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
103
103
  }
104
104
  const result: Array<TabBarToolbarItem | ReactTabBarToolbarItem> = [];
105
105
  for (const item of this.items.values()) {
106
- const visible = TabBarToolbarItem.is(item)
107
- ? this.commandRegistry.isVisible(item.command, widget)
108
- : (!item.isVisible || item.isVisible(widget));
109
- if (visible && (!item.when || this.contextKeyService.match(item.when, widget.node))) {
106
+ if (this.isItemVisible(item, widget)) {
110
107
  result.push(item);
111
108
  }
112
109
  }
@@ -139,6 +136,83 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
139
136
  return result;
140
137
  }
141
138
 
139
+ /**
140
+ * Query whether a toolbar `item` should be shown in the toolbar.
141
+ * This implementation delegates to item-specific checks according to their type.
142
+ *
143
+ * @param item a menu toolbar item
144
+ * @param widget the widget that is updating the toolbar
145
+ * @returns `false` if the `item` should be suppressed, otherwise `true`
146
+ */
147
+ protected isItemVisible(item: TabBarToolbarItem | ReactTabBarToolbarItem, widget: Widget): boolean {
148
+ if (TabBarToolbarItem.is(item) && item.command && !this.isTabBarToolbarItemVisible(item, widget)) {
149
+ return false;
150
+ }
151
+ if (MenuToolbarItem.is(item) && !this.isMenuToolbarItemVisible(item, widget)) {
152
+ return false;
153
+ }
154
+ if (AnyToolbarItem.isConditional(item) && !this.isConditionalItemVisible(item, widget)) {
155
+ return false;
156
+ }
157
+ // The item is not vetoed. Accept it
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Query whether a conditional toolbar `item` should be shown in the toolbar.
163
+ * This implementation delegates to the `item`'s own intrinsic conditionality.
164
+ *
165
+ * @param item a menu toolbar item
166
+ * @param widget the widget that is updating the toolbar
167
+ * @returns `false` if the `item` should be suppressed, otherwise `true`
168
+ */
169
+ protected isConditionalItemVisible(item: ConditionalToolbarItem, widget: Widget): boolean {
170
+ if (item.isVisible && !item.isVisible(widget)) {
171
+ return false;
172
+ }
173
+ if (item.when && !this.contextKeyService.match(item.when, widget.node)) {
174
+ return false;
175
+ }
176
+ return true;
177
+ }
178
+
179
+ /**
180
+ * Query whether a tab-bar toolbar `item` that has a command should be shown in the toolbar.
181
+ * This implementation returns `false` if the `item`'s command is not visible in the
182
+ * `widget` according to the command registry.
183
+ *
184
+ * @param item a tab-bar toolbar item that has a non-empty `command`
185
+ * @param widget the widget that is updating the toolbar
186
+ * @returns `false` if the `item` should be suppressed, otherwise `true`
187
+ */
188
+ protected isTabBarToolbarItemVisible(item: TabBarToolbarItem, widget: Widget): boolean {
189
+ return this.commandRegistry.isVisible(item.command, widget);
190
+ }
191
+
192
+ /**
193
+ * Query whether a menu toolbar `item` should be shown in the toolbar.
194
+ * This implementation returns `false` if the `item` does not have any actual menu to show.
195
+ *
196
+ * @param item a menu toolbar item
197
+ * @param widget the widget that is updating the toolbar
198
+ * @returns `false` if the `item` should be suppressed, otherwise `true`
199
+ */
200
+ protected isMenuToolbarItemVisible(item: MenuToolbarItem, widget: Widget): boolean {
201
+ const menu = this.menuRegistry.getMenu(item.menuPath);
202
+ const isVisible: (node: MenuNode) => boolean = node =>
203
+ node.children?.length
204
+ // Either the node is a sub-menu that has some visible child ...
205
+ ? node.children?.some(isVisible)
206
+ // ... or there is a command ...
207
+ : !!node.command
208
+ // ... that is visible ...
209
+ && this.commandRegistry.isVisible(node.command, widget)
210
+ // ... and a "when" clause does not suppress the menu node.
211
+ && (!node.when || this.contextKeyService.match(node.when, widget?.node));
212
+
213
+ return isVisible(menu);
214
+ }
215
+
142
216
  unregisterItem(itemOrId: TabBarToolbarItem | ReactTabBarToolbarItem | string): void {
143
217
  const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
144
218
  if (this.items.delete(id)) {
@@ -147,7 +221,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
147
221
  }
148
222
 
149
223
  registerMenuDelegate(menuPath: MenuPath, when?: string | ((widget: Widget) => boolean)): Disposable {
150
- const id = menuPath.join(menuDelegateSeparator);
224
+ const id = this.toElementId(menuPath);
151
225
  if (!this.menuDelegates.has(id)) {
152
226
  const isVisible: MenuDelegate['isVisible'] = !when
153
227
  ? yes
@@ -163,8 +237,20 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
163
237
  }
164
238
 
165
239
  unregisterMenuDelegate(menuPath: MenuPath): void {
166
- if (this.menuDelegates.delete(menuPath.join(menuDelegateSeparator))) {
240
+ if (this.menuDelegates.delete(this.toElementId(menuPath))) {
167
241
  this.fireOnDidChange();
168
242
  }
169
243
  }
244
+
245
+ /**
246
+ * Generate a single ID string from a menu path that
247
+ * is likely to be unique amongst the items in the toolbar.
248
+ *
249
+ * @param menuPath a menubar path
250
+ * @returns a likely unique ID based on the path
251
+ */
252
+ toElementId(menuPath: MenuPath): string {
253
+ return menuPath.join(menuDelegateSeparator);
254
+ }
255
+
170
256
  }
@@ -83,7 +83,7 @@ export interface MenuToolbarItem {
83
83
  menuPath: MenuPath;
84
84
  }
85
85
 
86
- interface ConditionalToolbarItem {
86
+ export interface ConditionalToolbarItem {
87
87
  /**
88
88
  * https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts
89
89
  */
@@ -130,6 +130,7 @@ export interface TabBarToolbarItem extends RegisteredToolbarItem,
130
130
  RenderedToolbarItem,
131
131
  Omit<ConditionalToolbarItem, 'isVisible'>,
132
132
  Pick<InlineToolbarItemMetadata, 'priority'>,
133
+ Partial<MenuToolbarItem>,
133
134
  Partial<MenuToolbarItemMetadata> { }
134
135
 
135
136
  /**
@@ -174,7 +175,33 @@ export namespace TabBarToolbarItem {
174
175
  }
175
176
 
176
177
  export namespace MenuToolbarItem {
178
+ /**
179
+ * Type guard for a toolbar item that actually is a menu item, amongst
180
+ * the other kinds of item that it may also be.
181
+ *
182
+ * @param item a toolbar item
183
+ * @returns whether the `item` is a menu item
184
+ */
185
+ export function is<T extends AnyToolbarItem>(item: T): item is T & MenuToolbarItem {
186
+ return Array.isArray(item.menuPath);
187
+ }
188
+
177
189
  export function getMenuPath(item: AnyToolbarItem): MenuPath | undefined {
178
190
  return Array.isArray(item.menuPath) ? item.menuPath : undefined;
179
191
  }
180
192
  }
193
+
194
+ export namespace AnyToolbarItem {
195
+ /**
196
+ * Type guard for a toolbar item that actually manifests any of the
197
+ * features of a conditional toolbar item.
198
+ *
199
+ * @param item a toolbar item
200
+ * @returns whether the `item` is a conditional item
201
+ */
202
+ export function isConditional<T extends AnyToolbarItem>(item: T): item is T & ConditionalToolbarItem {
203
+ return 'isVisible' in item && typeof item.isVisible === 'function'
204
+ || 'onDidChange' in item && typeof item.onDidChange === 'function'
205
+ || 'when' in item && typeof item.when === 'string';
206
+ }
207
+ }
@@ -22,7 +22,7 @@ import { Anchor, ContextMenuAccess, ContextMenuRenderer } from '../../context-me
22
22
  import { LabelIcon, LabelParser } from '../../label-parser';
23
23
  import { ACTION_ITEM, codicon, ReactWidget, Widget } from '../../widgets';
24
24
  import { TabBarToolbarRegistry } from './tab-bar-toolbar-registry';
25
- import { AnyToolbarItem, ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU } from './tab-bar-toolbar-types';
25
+ import { AnyToolbarItem, ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, MenuToolbarItem } from './tab-bar-toolbar-types';
26
26
  import { KeybindingRegistry } from '../..//keybinding';
27
27
 
28
28
  /**
@@ -33,6 +33,11 @@ export interface TabBarToolbarFactory {
33
33
  (): TabBarToolbar;
34
34
  }
35
35
 
36
+ /**
37
+ * Class name indicating rendering of a toolbar item without an icon but instead with a text label.
38
+ */
39
+ const NO_ICON_CLASS = 'no-icon';
40
+
36
41
  /**
37
42
  * Tab-bar toolbar widget representing the active [tab-bar toolbar items](TabBarToolbarItem).
38
43
  */
@@ -149,7 +154,9 @@ export class TabBarToolbar extends ReactWidget {
149
154
  this.keybindingContextKeys.clear();
150
155
  return <React.Fragment>
151
156
  {this.renderMore()}
152
- {[...this.inline.values()].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render(this.current))}
157
+ {[...this.inline.values()].map(item => TabBarToolbarItem.is(item)
158
+ ? (MenuToolbarItem.is(item) && !item.command ? this.renderMenuItem(item) : this.renderItem(item))
159
+ : item.render(this.current))}
153
160
  </React.Fragment>;
154
161
  }
155
162
 
@@ -176,8 +183,12 @@ export class TabBarToolbar extends ReactWidget {
176
183
  protected renderItem(item: AnyToolbarItem): React.ReactNode {
177
184
  let innerText = '';
178
185
  const classNames = [];
179
- if (item.text) {
180
- for (const labelPart of this.labelParser.parse(item.text)) {
186
+ const command = item.command ? this.commands.getCommand(item.command) : undefined;
187
+ // Fall back to the item ID in extremis so there is _something_ to render in the
188
+ // case that there is neither an icon nor a title
189
+ const itemText = item.text || command?.label || command?.id || item.id;
190
+ if (itemText) {
191
+ for (const labelPart of this.labelParser.parse(itemText)) {
181
192
  if (LabelIcon.is(labelPart)) {
182
193
  const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
183
194
  classNames.push(...className.split(' '));
@@ -186,13 +197,23 @@ export class TabBarToolbar extends ReactWidget {
186
197
  }
187
198
  }
188
199
  }
189
- const command = item.command ? this.commands.getCommand(item.command) : undefined;
190
- let iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon as string || (command && command.iconClass);
200
+ const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon as string || (command && command.iconClass);
191
201
  if (iconClass) {
192
- iconClass += ` ${ACTION_ITEM}`;
193
202
  classNames.push(iconClass);
194
203
  }
195
- const tooltip = `${item.tooltip || (command && command.label) || ''}${this.resolveKeybindingForCommand(command?.id)}`;
204
+ const tooltipText = item.tooltip || (command && command.label) || '';
205
+ const tooltip = `${this.labelParser.stripIcons(tooltipText)}${this.resolveKeybindingForCommand(command?.id)}`;
206
+
207
+ // Only present text if there is no icon
208
+ if (classNames.length) {
209
+ innerText = '';
210
+ } else if (innerText) {
211
+ // Make room for the label text
212
+ classNames.push(NO_ICON_CLASS);
213
+ }
214
+
215
+ // In any case, this is an action item, with or without icon.
216
+ classNames.push(ACTION_ITEM);
196
217
 
197
218
  const toolbarItemClassNames = this.getToolbarItemClassNames(item);
198
219
  return <div key={item.id}
@@ -224,6 +245,10 @@ export class TabBarToolbar extends ReactWidget {
224
245
  if (this.commandIsToggled(item.command)) {
225
246
  classNames.push('toggled');
226
247
  }
248
+ } else {
249
+ if (this.isEnabled(item)) {
250
+ classNames.push('enabled');
251
+ }
227
252
  }
228
253
  return classNames;
229
254
  }
@@ -281,6 +306,60 @@ export class TabBarToolbar extends ReactWidget {
281
306
  args: [this.current],
282
307
  anchor,
283
308
  context: this.current?.node,
309
+ onHide: () => toDisposeOnHide.dispose(),
310
+ skipSingleRootNode: true,
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Renders a toolbar item that is a menu, presenting it as a button with a little
316
+ * chevron decoration that pops up a floating menu when clicked.
317
+ *
318
+ * @param item a toolbar item that is a menu item
319
+ * @returns the rendered toolbar item
320
+ */
321
+ protected renderMenuItem(item: TabBarToolbarItem & MenuToolbarItem): React.ReactNode {
322
+ const icon = typeof item.icon === 'function' ? item.icon() : item.icon ?? 'ellipsis';
323
+ return <div key={item.id}
324
+ className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM + ' enabled menu'}
325
+ onClick={this.showPopupMenu.bind(this, item.menuPath)}>
326
+ <div id={item.id} className={codicon(icon, true)}
327
+ title={item.text} />
328
+ <div className={codicon('chevron-down') + ' chevron'} />
329
+ </div >;
330
+ }
331
+
332
+ /**
333
+ * Presents the menu to popup on the `event` that is the clicking of
334
+ * a menu toolbar item.
335
+ *
336
+ * @param menuPath the path of the registered menu to show
337
+ * @param event the mouse event triggering the menu
338
+ */
339
+ protected showPopupMenu = (menuPath: MenuPath, event: React.MouseEvent) => {
340
+ event.stopPropagation();
341
+ event.preventDefault();
342
+ const anchor = this.toAnchor(event);
343
+ this.renderPopupMenu(menuPath, anchor);
344
+ };
345
+
346
+ /**
347
+ * Renders the menu popped up on a menu toolbar item.
348
+ *
349
+ * @param menuPath the path of the registered menu to render
350
+ * @param anchor a description of where to render the menu
351
+ * @returns platform-specific access to the rendered context menu
352
+ */
353
+ protected renderPopupMenu(menuPath: MenuPath, anchor: Anchor): ContextMenuAccess {
354
+ const toDisposeOnHide = new DisposableCollection();
355
+ this.addClass('menu-open');
356
+ toDisposeOnHide.push(Disposable.create(() => this.removeClass('menu-open')));
357
+
358
+ return this.contextMenuRenderer.render({
359
+ menuPath,
360
+ args: [this.current],
361
+ anchor,
362
+ context: this.current?.node,
284
363
  onHide: () => toDisposeOnHide.dispose()
285
364
  });
286
365
  }
@@ -38,6 +38,7 @@ import { Root, createRoot } from 'react-dom/client';
38
38
  import { SelectComponent } from '../widgets/select-component';
39
39
  import { createElement } from 'react';
40
40
  import { PreviewableWidget } from '../widgets/previewable-widget';
41
+ import { EnhancedPreviewWidget } from '../widgets/enhanced-preview-widget';
41
42
 
42
43
  /** The class name added to hidden content nodes, which are required to render vertical side bars. */
43
44
  const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
@@ -504,7 +505,13 @@ export class TabBarRenderer extends TabBar.Renderer {
504
505
  labelElement.classList.add('theia-horizontal-tabBar-hover-title');
505
506
  labelElement.textContent = title.label;
506
507
  hoverBox.append(labelElement);
507
- if (title.caption) {
508
+ const widget = title.owner;
509
+ if (EnhancedPreviewWidget.is(widget)) {
510
+ const enhancedPreviewNode = widget.getEnhancedPreviewNode();
511
+ if (enhancedPreviewNode) {
512
+ hoverBox.appendChild(enhancedPreviewNode);
513
+ }
514
+ } else if (title.caption) {
508
515
  const captionElement = document.createElement('p');
509
516
  captionElement.classList.add('theia-horizontal-tabBar-hover-caption');
510
517
  captionElement.textContent = title.caption;
@@ -254,8 +254,8 @@
254
254
  }
255
255
 
256
256
  .p-TabBar.theia-app-centers
257
- .p-TabBar-tab:hover.p-mod-closable
258
- > .p-TabBar-tabCloseIcon {
257
+ .p-TabBar-tab.p-mod-closable
258
+ > .p-TabBar-tabCloseIcon:hover {
259
259
  border-radius: 5px;
260
260
  background-color: rgba(50%, 50%, 50%, 0.2);
261
261
  }
@@ -459,6 +459,10 @@
459
459
  cursor: pointer;
460
460
  }
461
461
 
462
+ .p-TabBar-toolbar .item.enabled .action-label::before {
463
+ display: flex;
464
+ }
465
+
462
466
  .p-TabBar-toolbar :not(.item.enabled) .action-label {
463
467
  background: transparent;
464
468
  cursor: default;
@@ -476,6 +480,11 @@
476
480
  line-height: 18px;
477
481
  }
478
482
 
483
+ .p-TabBar-toolbar .item > div.no-icon {
484
+ /* Make room for a text label instead of an icon. */
485
+ width: 100%;
486
+ }
487
+
479
488
  .p-TabBar-toolbar .item .collapse-all {
480
489
  background: var(--theia-icon-collapse-all) no-repeat;
481
490
  }
@@ -496,6 +505,27 @@
496
505
  background: var(--theia-icon-close) no-repeat;
497
506
  }
498
507
 
508
+ /** Configure layout of a toolbar item that shows a pop-up menu. */
509
+ .p-TabBar-toolbar .item.menu {
510
+ display: grid;
511
+ }
512
+
513
+ /** The elements of the item that shows a pop-up menu are stack atop one other. */
514
+ .p-TabBar-toolbar .item.menu > div {
515
+ grid-area: 1 / 1;
516
+ }
517
+
518
+ /**
519
+ * The chevron for the pop-up menu indication is shrunk and
520
+ * stuffed in the bottom-right corner.
521
+ */
522
+ .p-TabBar-toolbar .item.menu > .chevron {
523
+ scale: 50%;
524
+ align-self: end;
525
+ justify-self: end;
526
+ translate: 5px 3px;
527
+ }
528
+
499
529
  #theia-main-content-panel
500
530
  .p-TabBar:not(.theia-tabBar-active)
501
531
  .p-TabBar-toolbar {
@@ -472,6 +472,14 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
472
472
  return this.tree.markAsBusy(node, ms, token);
473
473
  }
474
474
 
475
+ get onDidUpdate(): Event<TreeNode[]> {
476
+ return this.tree.onDidUpdate;
477
+ }
478
+
479
+ markAsChecked(node: TreeNode, checked: boolean): void {
480
+ this.tree.markAsChecked(node, checked);
481
+ }
482
+
475
483
  }
476
484
  export namespace TreeModelImpl {
477
485
  export interface State {
@@ -243,12 +243,16 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
243
243
  }),
244
244
  ]);
245
245
  }
246
+ this.node.addEventListener('mousedown', this.handleMiddleClickEvent.bind(this));
247
+ this.node.addEventListener('mouseup', this.handleMiddleClickEvent.bind(this));
248
+ this.node.addEventListener('auxclick', this.handleMiddleClickEvent.bind(this));
246
249
  this.toDispose.pushAll([
247
250
  this.model,
248
251
  this.model.onChanged(() => this.updateRows()),
249
252
  this.model.onSelectionChanged(() => this.scheduleUpdateScrollToRow({ resize: false })),
250
253
  this.focusService.onDidChangeFocus(() => this.scheduleUpdateScrollToRow({ resize: false })),
251
254
  this.model.onDidChangeBusy(() => this.update()),
255
+ this.model.onDidUpdate(() => this.update()),
252
256
  this.model.onNodeRefreshed(() => this.updateDecorations()),
253
257
  this.model.onExpansionChanged(() => this.updateDecorations()),
254
258
  this.decoratorService,
@@ -575,6 +579,40 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
575
579
  </div>;
576
580
  }
577
581
 
582
+ /**
583
+ * Render the node expansion toggle.
584
+ * @param node the tree node.
585
+ * @param props the node properties.
586
+ */
587
+ protected renderCheckbox(node: TreeNode, props: NodeProps): React.ReactNode {
588
+ if (node.checkboxInfo === undefined) {
589
+ // eslint-disable-next-line no-null/no-null
590
+ return null;
591
+ }
592
+ return <input data-node-id={node.id}
593
+ readOnly
594
+ type='checkbox'
595
+ checked={!!node.checkboxInfo.checked}
596
+ title={node.checkboxInfo.tooltip}
597
+ aria-label={node.checkboxInfo.accessibilityInformation?.label}
598
+ role={node.checkboxInfo.accessibilityInformation?.role}
599
+ className='theia-input'
600
+ onClick={event => this.toggleChecked(event)} />;
601
+ }
602
+
603
+ protected toggleChecked(event: React.MouseEvent<HTMLElement>): void {
604
+ const nodeId = event.currentTarget.getAttribute('data-node-id');
605
+ if (nodeId) {
606
+ const node = this.model.getNode(nodeId);
607
+ if (node) {
608
+ this.model.markAsChecked(node, !node.checkboxInfo!.checked);
609
+ } else {
610
+ this.handleClickEvent(node, event);
611
+ }
612
+ }
613
+ event.preventDefault();
614
+ event.stopPropagation();
615
+ }
578
616
  /**
579
617
  * Render the tree node caption given the node properties.
580
618
  * @param node the tree node.
@@ -902,6 +940,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
902
940
  const attributes = this.createNodeAttributes(node, props);
903
941
  const content = <div className={TREE_NODE_CONTENT_CLASS}>
904
942
  {this.renderExpansionToggle(node, props)}
943
+ {this.renderCheckbox(node, props)}
905
944
  {this.decorateIcon(node, this.renderIcon(node, props))}
906
945
  {this.renderCaptionAffixes(node, props, 'captionPrefixes')}
907
946
  {this.renderCaption(node, props)}
@@ -924,6 +963,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
924
963
  style,
925
964
  onClick: event => this.handleClickEvent(node, event),
926
965
  onDoubleClick: event => this.handleDblClickEvent(node, event),
966
+ onAuxClick: event => this.handleAuxClickEvent(node, event),
927
967
  onContextMenu: event => this.handleContextMenuEvent(node, event),
928
968
  };
929
969
  }
@@ -1238,6 +1278,32 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
1238
1278
  event.stopPropagation();
1239
1279
  }
1240
1280
 
1281
+ /**
1282
+ * Handle the middle-click mouse event.
1283
+ * @param node the tree node if available.
1284
+ * @param event the middle-click mouse event.
1285
+ */
1286
+ protected handleAuxClickEvent(node: TreeNode | undefined, event: React.MouseEvent<HTMLElement>): void {
1287
+ if (event.button === 1) {
1288
+ this.model.openNode(node);
1289
+ if (SelectableTreeNode.is(node)) {
1290
+ this.model.selectNode(node);
1291
+ }
1292
+ }
1293
+ event.stopPropagation();
1294
+ }
1295
+
1296
+ /**
1297
+ * Handle the middle-click mouse event.
1298
+ * @param event the middle-click mouse event.
1299
+ */
1300
+ protected handleMiddleClickEvent(event: MouseEvent): void {
1301
+ // Prevents auto-scrolling behavior when middle-clicking.
1302
+ if (event.button === 1) {
1303
+ event.preventDefault();
1304
+ }
1305
+ }
1306
+
1241
1307
  /**
1242
1308
  * Handle the context menu click event.
1243
1309
  * - The context menu click event is triggered by the right-click.
@@ -20,6 +20,7 @@ import { Disposable, DisposableCollection } from '../../common/disposable';
20
20
  import { CancellationToken, CancellationTokenSource } from '../../common/cancellation';
21
21
  import { timeout } from '../../common/promise-util';
22
22
  import { isObject, Mutable } from '../../common';
23
+ import { AccessibilityInformation } from '../../common/accessibility';
23
24
 
24
25
  export const Tree = Symbol('Tree');
25
26
 
@@ -70,6 +71,19 @@ export interface Tree extends Disposable {
70
71
  * A token source of the given token should be canceled to unmark.
71
72
  */
72
73
  markAsBusy(node: Readonly<TreeNode>, ms: number, token: CancellationToken): Promise<void>;
74
+
75
+ /**
76
+ * An update to the tree node occurred, but the tree structure remains unchanged
77
+ */
78
+ readonly onDidUpdate: Event<TreeNode[]>;
79
+
80
+ markAsChecked(node: TreeNode, checked: boolean): void;
81
+ }
82
+
83
+ export interface TreeViewItemCheckboxInfo {
84
+ checked: boolean;
85
+ tooltip?: string;
86
+ accessibilityInformation?: AccessibilityInformation
73
87
  }
74
88
 
75
89
  /**
@@ -120,6 +134,11 @@ export interface TreeNode {
120
134
  * Whether this node is busy. Greater than 0 then busy; otherwise not.
121
135
  */
122
136
  readonly busy?: number;
137
+
138
+ /**
139
+ * Whether this node is checked.
140
+ */
141
+ readonly checkboxInfo?: TreeViewItemCheckboxInfo;
123
142
  }
124
143
 
125
144
  export namespace TreeNode {
@@ -238,6 +257,8 @@ export class TreeImpl implements Tree {
238
257
 
239
258
  protected readonly onDidChangeBusyEmitter = new Emitter<TreeNode>();
240
259
  readonly onDidChangeBusy = this.onDidChangeBusyEmitter.event;
260
+ protected readonly onDidUpdateEmitter = new Emitter<TreeNode[]>();
261
+ readonly onDidUpdate = this.onDidUpdateEmitter.event;
241
262
 
242
263
  protected nodes: {
243
264
  [id: string]: Mutable<TreeNode> | undefined
@@ -368,6 +389,12 @@ export class TreeImpl implements Tree {
368
389
  await this.doMarkAsBusy(node, ms, token);
369
390
  }
370
391
  }
392
+
393
+ markAsChecked(node: Mutable<TreeNode>, checked: boolean): void {
394
+ node.checkboxInfo!.checked = checked;
395
+ this.onDidUpdateEmitter.fire([node]);
396
+ }
397
+
371
398
  protected async doMarkAsBusy(node: Mutable<TreeNode>, ms: number, token: CancellationToken): Promise<void> {
372
399
  try {
373
400
  await timeout(ms, token);
@@ -0,0 +1,27 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 STMicroelectronics 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 { isFunction, isObject } from '../../common';
18
+
19
+ export interface EnhancedPreviewWidget {
20
+ getEnhancedPreviewNode(): Node | undefined;
21
+ }
22
+
23
+ export namespace EnhancedPreviewWidget {
24
+ export function is(arg: unknown): arg is EnhancedPreviewWidget {
25
+ return isObject<EnhancedPreviewWidget>(arg) && isFunction(arg.getEnhancedPreviewNode);
26
+ }
27
+ }
@@ -51,7 +51,7 @@ export class WindowContribution implements CommandContribution, KeybindingContri
51
51
  }
52
52
 
53
53
  registerMenus(registry: MenuModelRegistry): void {
54
- registry.registerMenuAction(CommonMenus.FILE_NEW, {
54
+ registry.registerMenuAction(CommonMenus.FILE_NEW_TEXT, {
55
55
  commandId: WindowCommands.NEW_WINDOW.id,
56
56
  order: 'c'
57
57
  });
@@ -106,4 +106,24 @@ export namespace ArrayUtils {
106
106
  export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
107
107
  return <T[]>array.filter(e => !!e);
108
108
  }
109
+
110
+ /**
111
+ * groups array elements through a comparator function
112
+ * @param data array of elements to group
113
+ * @param compare comparator function: return of 0 means should group, anything above means not group
114
+ * @returns array of arrays with grouped elements
115
+ */
116
+ export function groupBy<T>(data: ReadonlyArray<T>, compare: (a: T, b: T) => number): T[][] {
117
+ const result: T[][] = [];
118
+ let currentGroup: T[] | undefined = undefined;
119
+ for (const element of data.slice(0).sort(compare)) {
120
+ if (!currentGroup || compare(currentGroup[0], element) !== 0) {
121
+ currentGroup = [element];
122
+ result.push(currentGroup);
123
+ } else {
124
+ currentGroup.push(element);
125
+ }
126
+ }
127
+ return result;
128
+ }
109
129
  }