@theia/core 1.71.0-next.4 → 1.71.0-next.41

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 (140) hide show
  1. package/README.md +13 -13
  2. package/i18n/nls.cs.json +4 -4
  3. package/i18n/nls.de.json +4 -4
  4. package/i18n/nls.es.json +4 -4
  5. package/i18n/nls.fr.json +4 -4
  6. package/i18n/nls.hu.json +4 -4
  7. package/i18n/nls.it.json +4 -4
  8. package/i18n/nls.ja.json +4 -4
  9. package/i18n/nls.ko.json +4 -4
  10. package/i18n/nls.pl.json +4 -4
  11. package/i18n/nls.pt-br.json +4 -4
  12. package/i18n/nls.ru.json +4 -4
  13. package/i18n/nls.tr.json +4 -4
  14. package/i18n/nls.zh-cn.json +4 -4
  15. package/i18n/nls.zh-tw.json +4 -4
  16. package/lib/browser/catalog.json +93 -5
  17. package/lib/browser/components/card.d.ts.map +1 -1
  18. package/lib/browser/components/card.js +11 -3
  19. package/lib/browser/components/card.js.map +1 -1
  20. package/lib/browser/keyboard/index.d.ts +1 -0
  21. package/lib/browser/keyboard/index.d.ts.map +1 -1
  22. package/lib/browser/keyboard/index.js +1 -0
  23. package/lib/browser/keyboard/index.js.map +1 -1
  24. package/lib/browser/keyboard/keyboard-utils.d.ts +17 -0
  25. package/lib/browser/keyboard/keyboard-utils.d.ts.map +1 -0
  26. package/lib/browser/keyboard/keyboard-utils.js +40 -0
  27. package/lib/browser/keyboard/keyboard-utils.js.map +1 -0
  28. package/lib/browser/messaging/messaging-frontend-module.d.ts.map +1 -1
  29. package/lib/browser/messaging/messaging-frontend-module.js +3 -0
  30. package/lib/browser/messaging/messaging-frontend-module.js.map +1 -1
  31. package/lib/browser/messaging/ws-connection-source.d.ts +2 -1
  32. package/lib/browser/messaging/ws-connection-source.d.ts.map +1 -1
  33. package/lib/browser/messaging/ws-connection-source.js +5 -2
  34. package/lib/browser/messaging/ws-connection-source.js.map +1 -1
  35. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts +1 -0
  36. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.d.ts.map +1 -1
  37. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js +10 -1
  38. package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.js.map +1 -1
  39. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.d.ts +2 -0
  40. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.d.ts.map +1 -1
  41. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.js +11 -2
  42. package/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item.js.map +1 -1
  43. package/lib/common/event.d.ts +16 -0
  44. package/lib/common/event.d.ts.map +1 -1
  45. package/lib/common/event.js +20 -2
  46. package/lib/common/event.js.map +1 -1
  47. package/lib/common/event.spec.js +63 -0
  48. package/lib/common/event.spec.js.map +1 -1
  49. package/lib/common/glob.d.ts +2 -0
  50. package/lib/common/glob.d.ts.map +1 -1
  51. package/lib/common/glob.js +8 -7
  52. package/lib/common/glob.js.map +1 -1
  53. package/lib/common/message-rpc/channel.d.ts +1 -1
  54. package/lib/common/message-rpc/channel.d.ts.map +1 -1
  55. package/lib/common/message-rpc/channel.js +20 -12
  56. package/lib/common/message-rpc/channel.js.map +1 -1
  57. package/lib/common/message-rpc/channel.spec.d.ts.map +1 -1
  58. package/lib/common/message-rpc/channel.spec.js +94 -0
  59. package/lib/common/message-rpc/channel.spec.js.map +1 -1
  60. package/lib/common/message-rpc/rpc-protocol.d.ts.map +1 -1
  61. package/lib/common/message-rpc/rpc-protocol.js +13 -3
  62. package/lib/common/message-rpc/rpc-protocol.js.map +1 -1
  63. package/lib/common/message-rpc/uint8-array-message-buffer.d.ts.map +1 -1
  64. package/lib/common/message-rpc/uint8-array-message-buffer.js +1 -1
  65. package/lib/common/message-rpc/uint8-array-message-buffer.js.map +1 -1
  66. package/lib/common/messaging/index.d.ts +1 -0
  67. package/lib/common/messaging/index.d.ts.map +1 -1
  68. package/lib/common/messaging/index.js +1 -0
  69. package/lib/common/messaging/index.js.map +1 -1
  70. package/lib/common/messaging/socket-write-buffer.d.ts +4 -3
  71. package/lib/common/messaging/socket-write-buffer.d.ts.map +1 -1
  72. package/lib/common/messaging/socket-write-buffer.js +14 -4
  73. package/lib/common/messaging/socket-write-buffer.js.map +1 -1
  74. package/lib/common/preferences/index.d.ts +1 -0
  75. package/lib/common/preferences/index.d.ts.map +1 -1
  76. package/lib/common/preferences/index.js +1 -0
  77. package/lib/common/preferences/index.js.map +1 -1
  78. package/lib/common/preferences/preference-utils.d.ts +6 -0
  79. package/lib/common/preferences/preference-utils.d.ts.map +1 -0
  80. package/lib/common/preferences/preference-utils.js +29 -0
  81. package/lib/common/preferences/preference-utils.js.map +1 -0
  82. package/lib/common/resource.d.ts +2 -0
  83. package/lib/common/resource.d.ts.map +1 -1
  84. package/lib/common/resource.js +7 -3
  85. package/lib/common/resource.js.map +1 -1
  86. package/lib/electron-browser/menu/electron-main-menu-factory.d.ts.map +1 -1
  87. package/lib/electron-browser/menu/electron-main-menu-factory.js +5 -1
  88. package/lib/electron-browser/menu/electron-main-menu-factory.js.map +1 -1
  89. package/lib/electron-browser/messaging/electron-messaging-frontend-module.d.ts.map +1 -1
  90. package/lib/electron-browser/messaging/electron-messaging-frontend-module.js +3 -0
  91. package/lib/electron-browser/messaging/electron-messaging-frontend-module.js.map +1 -1
  92. package/lib/electron-main/electron-api-main.d.ts.map +1 -1
  93. package/lib/electron-main/electron-api-main.js +4 -2
  94. package/lib/electron-main/electron-api-main.js.map +1 -1
  95. package/lib/electron-main/theia-electron-window.d.ts.map +1 -1
  96. package/lib/electron-main/theia-electron-window.js +3 -0
  97. package/lib/electron-main/theia-electron-window.js.map +1 -1
  98. package/lib/node/messaging/index.d.ts +1 -0
  99. package/lib/node/messaging/index.d.ts.map +1 -1
  100. package/lib/node/messaging/index.js +1 -0
  101. package/lib/node/messaging/index.js.map +1 -1
  102. package/lib/node/messaging/messaging-backend-module.d.ts.map +1 -1
  103. package/lib/node/messaging/messaging-backend-module.js +4 -0
  104. package/lib/node/messaging/messaging-backend-module.js.map +1 -1
  105. package/lib/node/messaging/websocket-frontend-connection-service.d.ts +9 -5
  106. package/lib/node/messaging/websocket-frontend-connection-service.d.ts.map +1 -1
  107. package/lib/node/messaging/websocket-frontend-connection-service.js +21 -5
  108. package/lib/node/messaging/websocket-frontend-connection-service.js.map +1 -1
  109. package/lib/node/process-utils.d.ts.map +1 -1
  110. package/lib/node/process-utils.js +9 -1
  111. package/lib/node/process-utils.js.map +1 -1
  112. package/package.json +32 -32
  113. package/src/browser/components/card.tsx +13 -2
  114. package/src/browser/keyboard/index.ts +1 -0
  115. package/src/browser/keyboard/keyboard-utils.ts +37 -0
  116. package/src/browser/messaging/messaging-frontend-module.ts +3 -0
  117. package/src/browser/messaging/ws-connection-source.ts +3 -2
  118. package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +14 -1
  119. package/src/browser/shell/tab-bar-toolbar/tab-toolbar-item.tsx +13 -2
  120. package/src/browser/style/card.css +4 -2
  121. package/src/common/event.spec.ts +80 -0
  122. package/src/common/event.ts +31 -2
  123. package/src/common/glob.ts +2 -2
  124. package/src/common/message-rpc/channel.spec.ts +116 -0
  125. package/src/common/message-rpc/channel.ts +15 -11
  126. package/src/common/message-rpc/rpc-protocol.ts +12 -3
  127. package/src/common/message-rpc/uint8-array-message-buffer.ts +1 -1
  128. package/src/common/messaging/index.ts +1 -0
  129. package/src/common/messaging/socket-write-buffer.ts +10 -4
  130. package/src/common/preferences/index.ts +1 -0
  131. package/src/common/preferences/preference-utils.ts +28 -0
  132. package/src/common/resource.ts +8 -2
  133. package/src/electron-browser/menu/electron-main-menu-factory.ts +5 -1
  134. package/src/electron-browser/messaging/electron-messaging-frontend-module.ts +3 -0
  135. package/src/electron-main/electron-api-main.ts +4 -2
  136. package/src/electron-main/theia-electron-window.ts +3 -0
  137. package/src/node/messaging/index.ts +1 -0
  138. package/src/node/messaging/messaging-backend-module.ts +5 -1
  139. package/src/node/messaging/websocket-frontend-connection-service.ts +20 -7
  140. package/src/node/process-utils.ts +9 -1
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@theia/core",
3
- "version": "1.71.0-next.4+cdbc982c6",
3
+ "version": "1.71.0-next.41+0c2a28923",
4
4
  "description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.",
5
5
  "main": "lib/common/index.js",
6
6
  "typings": "lib/common/index.d.ts",
7
7
  "dependencies": {
8
- "@babel/runtime": "^7.10.0",
8
+ "@babel/runtime": "^7.29.2",
9
9
  "@lumino/algorithm": "^2.0.4",
10
10
  "@lumino/commands": "^2.3.3",
11
11
  "@lumino/coreutils": "^2.2.2",
@@ -16,31 +16,31 @@
16
16
  "@lumino/signaling": "^2.1.5",
17
17
  "@lumino/virtualdom": "^2.0.4",
18
18
  "@lumino/widgets": "2.7.5",
19
- "@parcel/watcher": "^2.5.0",
20
- "@theia/application-package": "1.71.0-next.4+cdbc982c6",
21
- "@theia/request": "1.71.0-next.4+cdbc982c6",
22
- "@types/body-parser": "^1.16.4",
19
+ "@parcel/watcher": "^2.5.6",
20
+ "@theia/application-package": "1.71.0-next.41+0c2a28923",
21
+ "@theia/request": "1.71.0-next.41+0c2a28923",
22
+ "@types/body-parser": "^1.19.6",
23
23
  "@types/express": "^4.17.21",
24
24
  "@types/fs-extra": "^4.0.2",
25
25
  "@types/lodash.debounce": "4.0.3",
26
- "@types/lodash.throttle": "^4.1.3",
27
- "@types/markdown-it": "^14.1.0",
26
+ "@types/lodash.throttle": "^4.1.9",
27
+ "@types/markdown-it": "^14.1.2",
28
28
  "@types/markdown-it-emoji": "^3.0.1",
29
- "@types/react": "^18.0.15",
30
- "@types/react-dom": "^18.0.6",
31
- "@types/route-parser": "^0.1.1",
32
- "@types/safer-buffer": "^2.1.0",
29
+ "@types/react": "^18.3.28",
30
+ "@types/react-dom": "^18.3.7",
31
+ "@types/route-parser": "^0.1.7",
32
+ "@types/safer-buffer": "^2.1.3",
33
33
  "@types/uuid": "^9.0.8",
34
- "@types/ws": "^8.5.5",
34
+ "@types/ws": "^8.18.1",
35
35
  "@types/yargs": "^15",
36
36
  "@vscode/codicons": "0.0.45",
37
- "ajv": "^6.5.3",
38
- "async-mutex": "^0.4.0",
39
- "body-parser": "^1.17.2",
40
- "cookie": "^1.0.2",
41
- "dompurify": "^3.2.4",
37
+ "ajv": "^6.14.0",
38
+ "async-mutex": "^0.4.1",
39
+ "body-parser": "^1.20.4",
40
+ "cookie": "^1.1.1",
41
+ "dompurify": "^3.3.3",
42
42
  "drivelist": "^12.0.2",
43
- "express": "^4.21.0",
43
+ "express": "^4.22.1",
44
44
  "fast-json-stable-stringify": "^2.1.0",
45
45
  "file-icons-js": "~1.0.3",
46
46
  "font-awesome": "^4.7.0",
@@ -48,32 +48,32 @@
48
48
  "fuzzy": "^0.1.3",
49
49
  "http-proxy-agent": "^5.0.0",
50
50
  "https-proxy-agent": "^5.0.0",
51
- "iconv-lite": "^0.6.0",
52
- "inversify": "^6.1.3",
53
- "jschardet": "^2.1.1",
51
+ "iconv-lite": "^0.6.3",
52
+ "inversify": "^6.2.2",
53
+ "jschardet": "^2.3.0",
54
54
  "keytar": "7.9.0",
55
55
  "lodash.debounce": "^4.0.8",
56
56
  "lodash.throttle": "^4.1.1",
57
- "markdown-it": "^14.1.0",
57
+ "markdown-it": "^14.1.1",
58
58
  "markdown-it-anchor": "^9.2.0",
59
59
  "markdown-it-emoji": "^3.0.0",
60
- "msgpackr": "^1.10.2",
60
+ "msgpackr": "^1.11.9",
61
61
  "p-debounce": "^2.1.0",
62
62
  "perfect-scrollbar": "1.5.5",
63
- "react": "^18.2.0",
64
- "react-dom": "^18.2.0",
65
- "react-tooltip": "^4.2.21",
66
- "react-virtuoso": "^2.17.0",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
65
+ "react-tooltip": "^4.5.1",
66
+ "react-virtuoso": "^2.19.1",
67
67
  "reflect-metadata": "^0.2.2",
68
68
  "route-parser": "^0.0.5",
69
69
  "safer-buffer": "^2.1.2",
70
- "socket.io": "^4.5.3",
71
- "socket.io-client": "^4.5.3",
70
+ "socket.io": "^4.8.3",
71
+ "socket.io-client": "^4.8.3",
72
72
  "tslib": "^2.6.2",
73
73
  "uuid": "^9.0.1",
74
74
  "vscode-languageserver-protocol": "3.17.5",
75
75
  "vscode-uri": "3.0.8",
76
- "ws": "^8.17.1",
76
+ "ws": "^8.20.0",
77
77
  "yargs": "^15.3.1"
78
78
  },
79
79
  "peerDependencies": {
@@ -221,5 +221,5 @@
221
221
  "nyc": {
222
222
  "extends": "../../configs/nyc.json"
223
223
  },
224
- "gitHead": "cdbc982c6456e257ba7ed8e01b8c71aa7ac67283"
224
+ "gitHead": "0c2a289231e3c0186367d33cbe2d357ddb437ef3"
225
225
  }
@@ -15,6 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as React from 'react';
18
+ import { buttonKeyboardProps, isActivationKey } from '../keyboard/keyboard-utils';
18
19
 
19
20
  export interface CardActionButton {
20
21
  /** Icon class (e.g., codicon) */
@@ -69,14 +70,22 @@ export const Card = React.memo(function Card(props: CardProps): React.ReactEleme
69
70
  } = props;
70
71
 
71
72
  const isInteractive = onClick !== undefined;
73
+ const [hasFocus, setHasFocus] = React.useState(false);
72
74
 
73
75
  const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
74
- if (onClick && (e.key === 'Enter' || e.key === ' ')) {
76
+ if (onClick && isActivationKey(e)) {
75
77
  e.preventDefault();
76
78
  onClick();
77
79
  }
78
80
  }, [onClick]);
79
81
 
82
+ const handleFocus = React.useCallback(() => setHasFocus(true), []);
83
+ const handleBlur = React.useCallback((e: React.FocusEvent<HTMLDivElement>) => {
84
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
85
+ setHasFocus(false);
86
+ }
87
+ }, []);
88
+
80
89
  const cardClasses = [
81
90
  'theia-Card',
82
91
  isInteractive && 'theia-Card-interactive',
@@ -94,6 +103,8 @@ export const Card = React.memo(function Card(props: CardProps): React.ReactEleme
94
103
  role={isInteractive ? 'button' : undefined}
95
104
  tabIndex={isInteractive ? 0 : undefined}
96
105
  onKeyDown={isInteractive ? handleKeyDown : undefined}
106
+ onFocus={actionButtons ? handleFocus : undefined}
107
+ onBlur={actionButtons ? handleBlur : undefined}
97
108
  >
98
109
  {icon && (
99
110
  <div className={`theia-Card-icon ${icon}`}></div>
@@ -115,7 +126,7 @@ export const Card = React.memo(function Card(props: CardProps): React.ReactEleme
115
126
  key={i}
116
127
  className={`theia-Card-action-btn ${btn.iconClass}`}
117
128
  title={btn.title}
118
- aria-label={btn.title}
129
+ {...buttonKeyboardProps(btn.title, hasFocus ? 0 : -1)}
119
130
  onClick={btn.onClick}
120
131
  />
121
132
  ))}
@@ -18,3 +18,4 @@ export * from './keys';
18
18
  export * from './keyboard-layout-service';
19
19
  export * from './browser-keyboard-layout-provider';
20
20
  export * from './browser-keyboard-frontend-contribution';
21
+ export * from './keyboard-utils';
@@ -0,0 +1,37 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource 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
+ /**
18
+ * Query whether the keyboard event represents an element activation.
19
+ * That is, whether the user pressed `Enter` or `Space` on a focusable
20
+ * element with `role="button"` or similar interactive role.
21
+ */
22
+ export function isActivationKey(e: React.KeyboardEvent | KeyboardEvent): boolean {
23
+ return e.key === 'Enter' || e.key === ' ';
24
+ }
25
+
26
+ /**
27
+ * Returns the ARIA/accessibility props that make a non-button HTML element
28
+ * keyboard-navigable and screen-reader-accessible as a button.
29
+ *
30
+ * Spread these onto the element alongside `onClick` and `onKeyDown`:
31
+ * ```tsx
32
+ * <div {...buttonKeyboardProps(label)} onClick={handler} onKeyDown={e => isActivationKey(e) && handler()} />
33
+ * ```
34
+ */
35
+ export function buttonKeyboardProps(ariaLabel: string, tabIndex = 0): React.HTMLAttributes<HTMLElement> {
36
+ return { tabIndex, role: 'button', 'aria-label': ariaLabel };
37
+ }
@@ -21,10 +21,13 @@ import { LocalConnectionProvider, RemoteConnectionProvider, ServiceConnectionPro
21
21
  import { ConnectionSource } from './connection-source';
22
22
  import { ConnectionCloseService, connectionCloseServicePath } from '../../common/messaging/connection-management';
23
23
  import { WebSocketConnectionProvider } from './ws-connection-provider';
24
+ import { SocketWriteBuffer } from '../../common/messaging/socket-write-buffer';
24
25
 
25
26
  const backendServiceProvider = Symbol('backendServiceProvider');
26
27
 
27
28
  export const messagingFrontendModule = new ContainerModule(bind => {
29
+ // Transient: each connection source gets its own private buffer instance.
30
+ bind(SocketWriteBuffer).toSelf();
28
31
  bind(ConnectionCloseService).toDynamicValue(ctx => WebSocketConnectionProvider.createProxy(ctx.container, connectionCloseServicePath)).inSingletonScope();
29
32
  bind(BrowserFrontendIdProvider).toSelf().inSingletonScope();
30
33
  bind(FrontendIdProvider).toService(BrowserFrontendIdProvider);
@@ -33,7 +33,8 @@ export class WebSocketConnectionSource implements ConnectionSource {
33
33
  @inject(FrontendIdProvider)
34
34
  protected readonly frontendIdProvider: FrontendIdProvider;
35
35
 
36
- private readonly writeBuffer = new SocketWriteBuffer();
36
+ @inject(SocketWriteBuffer)
37
+ protected readonly writeBuffer: SocketWriteBuffer;
37
38
 
38
39
  private _socket: Socket;
39
40
  get socket(): Socket {
@@ -135,8 +136,8 @@ export class WebSocketConnectionSource implements ConnectionSource {
135
136
 
136
137
  connectNewChannel(): void {
137
138
  if (this.currentChannel) {
138
- this.currentChannel.close();
139
139
  this.currentChannel.onCloseEmitter.fire({ reason: 'reconnecting channel' });
140
+ this.currentChannel.close();
140
141
  }
141
142
  this.writeBuffer.drain();
142
143
  this.currentChannel = this.createChannel();
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { inject, injectable, postConstruct } from 'inversify';
18
18
  import * as React from 'react';
19
+ import { buttonKeyboardProps, isActivationKey } from '../../keyboard/keyboard-utils';
19
20
  import { ContextKeyService } from '../../context-key-service';
20
21
  import { CommandRegistry, Disposable, DisposableCollection, nls } from '../../../common';
21
22
  import { Anchor, ContextMenuAccess, ContextMenuRenderer } from '../../context-menu-renderer';
@@ -150,7 +151,10 @@ export class TabBarToolbar extends ReactWidget {
150
151
 
151
152
  protected renderMore(): React.ReactNode {
152
153
  return !!this.more.size && <div key='__more__' className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM + ' enabled'}>
153
- <div id='__more__' className={codicon('ellipsis', true)} onClick={this.showMoreContextMenu}
154
+ <div id='__more__' className={codicon('ellipsis', true)}
155
+ {...buttonKeyboardProps(nls.localizeByDefault('More Actions...'))}
156
+ onClick={this.showMoreContextMenu}
157
+ onKeyDown={this.handleMoreKeyDown}
154
158
  title={nls.localizeByDefault('More Actions...')} />
155
159
  </div>;
156
160
  }
@@ -162,6 +166,15 @@ export class TabBarToolbar extends ReactWidget {
162
166
  this.renderMoreContextMenu(anchor);
163
167
  };
164
168
 
169
+ protected handleMoreKeyDown = (event: React.KeyboardEvent) => {
170
+ if (isActivationKey(event)) {
171
+ event.preventDefault();
172
+ event.stopPropagation();
173
+ const { left, bottom } = (event.currentTarget as HTMLElement).getBoundingClientRect();
174
+ this.renderMoreContextMenu({ x: left, y: bottom });
175
+ }
176
+ };
177
+
165
178
  renderMoreContextMenu(anchor: Anchor): ContextMenuAccess {
166
179
  const toDisposeOnHide = new DisposableCollection();
167
180
  this.addClass('menu-open');
@@ -23,6 +23,7 @@ import { KeybindingRegistry } from '../../keybinding';
23
23
  import { ACTION_ITEM } from '../../widgets';
24
24
  import { TabBarToolbar } from './tab-bar-toolbar';
25
25
  import * as React from 'react';
26
+ import { buttonKeyboardProps, isActivationKey } from '../../keyboard/keyboard-utils';
26
27
  import { ActionMenuNode, GroupImpl, MenuNode } from '../../../common/menu';
27
28
 
28
29
  export interface TabBarToolbarItem {
@@ -195,16 +196,24 @@ export class RenderedToolbarItemImpl extends AbstractToolbarItemImpl<RenderedToo
195
196
  };
196
197
 
197
198
  protected executeCommand(e: React.MouseEvent<HTMLElement>, widget: Widget): void {
199
+ this.doExecuteCommand(e, widget);
200
+ };
201
+
202
+ protected doExecuteCommand(e: React.SyntheticEvent<HTMLElement>, widget: Widget): void {
198
203
  e.preventDefault();
199
204
  e.stopPropagation();
200
-
201
205
  if (!this.isEnabled(widget)) {
202
206
  return;
203
207
  }
204
-
205
208
  if (this.action.command) {
206
209
  this.commandRegistry.executeCommand(this.action.command, widget);
207
210
  }
211
+ }
212
+
213
+ protected onKeyDownEvent = (e: React.KeyboardEvent<HTMLElement>, widget: Widget) => {
214
+ if (isActivationKey(e)) {
215
+ this.doExecuteCommand(e, widget);
216
+ }
208
217
  };
209
218
 
210
219
  protected renderItem(widget: Widget): React.ReactNode {
@@ -249,7 +258,9 @@ export class RenderedToolbarItemImpl extends AbstractToolbarItemImpl<RenderedToo
249
258
  onMouseUp={this.onMouseUpEvent}
250
259
  onMouseOut={this.onMouseUpEvent} >
251
260
  <div id={this.action.id} className={classNames.join(' ')}
261
+ {...buttonKeyboardProps(tooltip)}
252
262
  onClick={e => this.executeCommand(e, widget)}
263
+ onKeyDown={e => this.onKeyDownEvent(e, widget)}
253
264
  title={tooltip} > {innerText}
254
265
  </div>
255
266
  </div>;
@@ -101,11 +101,13 @@
101
101
  transition: opacity 0.15s;
102
102
  }
103
103
 
104
- .theia-Card:hover .theia-Card-footer-time {
104
+ .theia-Card:hover .theia-Card-footer-time,
105
+ .theia-Card:focus-within .theia-Card-footer-time {
105
106
  opacity: 0;
106
107
  }
107
108
 
108
- .theia-Card:hover .theia-Card-footer-actions {
109
+ .theia-Card:hover .theia-Card-footer-actions,
110
+ .theia-Card:focus-within .theia-Card-footer-actions {
109
111
  opacity: 1;
110
112
  pointer-events: auto;
111
113
  }
@@ -29,4 +29,84 @@ describe('Event Objects', () => {
29
29
  expect(counter).eq(1);
30
30
  });
31
31
 
32
+ describe('Emitter errorHandling option', () => {
33
+
34
+ it('should log errors by default', () => {
35
+ const emitter = new Emitter<void>();
36
+ const errors: unknown[] = [];
37
+ const originalError = console.error;
38
+ console.error = (e: unknown) => errors.push(e);
39
+
40
+ try {
41
+ emitter.event(() => { throw new Error('test error'); });
42
+ emitter.fire(undefined);
43
+
44
+ expect(errors).to.have.lengthOf(1);
45
+ expect((errors[0] as Error).message).to.equal('test error');
46
+ } finally {
47
+ console.error = originalError;
48
+ }
49
+ });
50
+
51
+ it('should propagate a single error when errorHandling is propagate', () => {
52
+ const emitter = new Emitter<void>({ errorHandling: 'propagate' });
53
+
54
+ emitter.event(() => { throw new Error('boom'); });
55
+
56
+ expect(() => emitter.fire(undefined)).to.throw('boom');
57
+ });
58
+
59
+ it('should call all listeners before propagating the error', () => {
60
+ const emitter = new Emitter<void>({ errorHandling: 'propagate' });
61
+ let secondCalled = false;
62
+
63
+ emitter.event(() => { throw new Error('first fails'); });
64
+ emitter.event(() => { secondCalled = true; });
65
+
66
+ expect(() => emitter.fire(undefined)).to.throw('first fails');
67
+ expect(secondCalled).to.be.true;
68
+ });
69
+
70
+ it('should throw AggregateError when multiple listeners fail with propagate', () => {
71
+ const emitter = new Emitter<void>({ errorHandling: 'propagate' });
72
+
73
+ emitter.event(() => { throw new Error('error 1'); });
74
+ emitter.event(() => { throw new Error('error 2'); });
75
+
76
+ try {
77
+ emitter.fire(undefined);
78
+ expect.fail('Expected an error to be thrown');
79
+ } catch (err) {
80
+ expect(err).to.be.instanceOf(AggregateError);
81
+ const aggregate = err as AggregateError;
82
+ expect(aggregate.errors).to.have.lengthOf(2);
83
+ expect(aggregate.errors[0].message).to.equal('error 1');
84
+ expect(aggregate.errors[1].message).to.equal('error 2');
85
+ }
86
+ });
87
+
88
+ it('should invoke custom error handler for each error', () => {
89
+ const errors: unknown[] = [];
90
+ const emitter = new Emitter<void>({ errorHandling: e => errors.push(e) });
91
+
92
+ emitter.event(() => { throw new Error('handled 1'); });
93
+ emitter.event(() => { throw new Error('handled 2'); });
94
+ emitter.fire(undefined);
95
+
96
+ expect(errors).to.have.lengthOf(2);
97
+ expect((errors[0] as Error).message).to.equal('handled 1');
98
+ expect((errors[1] as Error).message).to.equal('handled 2');
99
+ });
100
+
101
+ it('should not throw when no listeners fail with propagate', () => {
102
+ const emitter = new Emitter<void>({ errorHandling: 'propagate' });
103
+ let called = false;
104
+
105
+ emitter.event(() => { called = true; });
106
+ emitter.fire(undefined);
107
+
108
+ expect(called).to.be.true;
109
+ });
110
+ });
111
+
32
112
  });
@@ -123,6 +123,8 @@ class CallbackList implements Iterable<Callback> {
123
123
  private _callbacks: Function[] | undefined;
124
124
  private _contexts: any[] | undefined;
125
125
 
126
+ constructor(private readonly errorHandling: ErrorHandlingStrategy = 'log') {}
127
+
126
128
  get length(): number {
127
129
  return this._callbacks && this._callbacks.length || 0;
128
130
  }
@@ -179,13 +181,25 @@ class CallbackList implements Iterable<Callback> {
179
181
 
180
182
  public invoke(...args: any[]): any[] {
181
183
  const ret: any[] = [];
184
+ const errors: unknown[] = [];
182
185
  for (const callback of this) {
183
186
  try {
184
187
  ret.push(callback(...args));
185
188
  } catch (e) {
186
- console.error(e);
189
+ if (this.errorHandling === 'propagate') {
190
+ errors.push(e);
191
+ } else if (typeof this.errorHandling === 'function') {
192
+ this.errorHandling(e);
193
+ } else {
194
+ console.error(e);
195
+ }
187
196
  }
188
197
  }
198
+ if (errors.length === 1) {
199
+ throw errors[0];
200
+ } else if (errors.length > 1) {
201
+ throw new AggregateError(errors, 'Multiple event listeners failed');
202
+ }
189
203
  return ret;
190
204
  }
191
205
 
@@ -199,9 +213,24 @@ class CallbackList implements Iterable<Callback> {
199
213
  }
200
214
  }
201
215
 
216
+ /**
217
+ * A strategy for handling errors in firing an emitter's event, one of
218
+ *
219
+ * - `'log'` (default): errors are caught and logged via `console.error`
220
+ * - `'propagate'`: all listeners are called; if any throw, the errors are collected
221
+ * and re-thrown after all listeners complete (single error re-thrown directly,
222
+ * multiple errors wrapped in an `AggregateError`)
223
+ * - `(error: unknown) => void`: a custom callback invoked for each error
224
+ */
225
+ export type ErrorHandlingStrategy = 'log' | 'propagate' | ((error: unknown) => void);
226
+
202
227
  export interface EmitterOptions {
203
228
  onFirstListenerAdd?: Function;
204
229
  onLastListenerRemove?: Function;
230
+ /**
231
+ * How errors thrown by event listeners are handled during {@link Emitter.fire}.
232
+ */
233
+ errorHandling?: ErrorHandlingStrategy;
205
234
  }
206
235
 
207
236
  export class Emitter<T = any> {
@@ -229,7 +258,7 @@ export class Emitter<T = any> {
229
258
  if (!this._event) {
230
259
  this._event = Object.assign((listener: (e: T) => any, thisArgs?: any, disposables?: DisposableGroup) => {
231
260
  if (!this._callbacks) {
232
- this._callbacks = new CallbackList();
261
+ this._callbacks = new CallbackList(this._options?.errorHandling);
233
262
  }
234
263
  if (this._options && this._options.onFirstListenerAdd && this._callbacks.isEmpty()) {
235
264
  this._options.onFirstListenerAdd(this);
@@ -44,8 +44,8 @@ export interface SiblingClause {
44
44
  when: string;
45
45
  }
46
46
 
47
- const GLOBSTAR = '**';
48
- const GLOB_SPLIT = '/';
47
+ export const GLOBSTAR = '**';
48
+ export const GLOB_SPLIT = '/';
49
49
  const PATH_REGEX = '[/\\\\]'; // any slash or backslash
50
50
  const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash
51
51
  const ALL_FORWARD_SLASHES = /\//g;
@@ -18,6 +18,7 @@ import { assert, expect, spy, use } from 'chai';
18
18
  import * as spies from 'chai-spies';
19
19
  import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from './uint8-array-message-buffer';
20
20
  import { ChannelMultiplexer, ForwardingChannel, MessageProvider } from './channel';
21
+ import { RpcProtocol } from './rpc-protocol';
21
22
 
22
23
  use(spies);
23
24
 
@@ -84,5 +85,120 @@ describe('Message Channel', () => {
84
85
 
85
86
  expect(openChannelSpy).to.be.called.exactly(4);
86
87
  });
88
+
89
+ it('should reject pending open() promises when underlying channel closes', async () => {
90
+ const pipe = new ChannelPipe();
91
+ const leftMultiplexer = new ChannelMultiplexer(pipe.left);
92
+ // Don't create a right multiplexer, so no AckOpen will arrive
93
+
94
+ const openPromise = leftMultiplexer.open('test');
95
+
96
+ // Close the underlying channel
97
+ pipe.left.onCloseEmitter.fire({ reason: 'test close' });
98
+
99
+ // The open promise should reject, not hang forever
100
+ try {
101
+ await openPromise;
102
+ assert.fail('Expected open() promise to be rejected');
103
+ } catch (err) {
104
+ expect(err).to.be.instanceOf(Error);
105
+ expect((err as Error).message).to.contain('test close');
106
+ }
107
+ });
108
+
109
+ it('should fire onClose on sub-channels when underlying channel closes', async () => {
110
+ const pipe = new ChannelPipe();
111
+ const leftMultiplexer = new ChannelMultiplexer(pipe.left);
112
+ const rightMultiplexer = new ChannelMultiplexer(pipe.right);
113
+
114
+ const leftChannel = await leftMultiplexer.open('test');
115
+ const rightChannel = rightMultiplexer.getOpenChannel('test');
116
+ assert.isDefined(rightChannel);
117
+
118
+ const leftCloseSpy = spy(() => { });
119
+ leftChannel.onClose(leftCloseSpy);
120
+
121
+ // Close the underlying channel from the remote side
122
+ pipe.left.onCloseEmitter.fire({ reason: 'underlying closed' });
123
+
124
+ expect(leftCloseSpy).to.have.been.called();
125
+ });
126
+ });
127
+
128
+ describe('Channel close event ordering', () => {
129
+ it('should not deliver onClose after close() has been called', () => {
130
+ const channel = new ForwardingChannel('test', () => { }, () => new Uint8ArrayWriteBuffer());
131
+
132
+ const closeSpy = spy(() => { });
133
+ channel.onClose(closeSpy);
134
+
135
+ // Bug pattern: close() first (disposes emitters), then fire (no-op)
136
+ channel.close();
137
+ channel.onCloseEmitter.fire({ reason: 'too late' });
138
+
139
+ // The listener should not be called because close() already disposed the emitter
140
+ expect(closeSpy).to.not.have.been.called();
141
+ });
142
+
143
+ it('should deliver onClose when fired before close()', () => {
144
+ const channel = new ForwardingChannel('test', () => { }, () => new Uint8ArrayWriteBuffer());
145
+
146
+ const closeSpy = spy(() => { });
147
+ channel.onClose(closeSpy);
148
+
149
+ // Correct pattern: fire first, then close
150
+ channel.onCloseEmitter.fire({ reason: 'proper close' });
151
+ channel.close();
152
+
153
+ expect(closeSpy).to.have.been.called();
154
+ });
155
+ });
156
+
157
+ describe('RPC protocol with write buffer overflow', () => {
158
+ it('should reject the promise when commit fails due to buffer overflow', async () => {
159
+ // Simulate a channel whose write buffer throws on commit (e.g. SocketWriteBuffer overflow)
160
+ const channel = new ForwardingChannel('test', () => { }, () => {
161
+ const buffer = new Uint8ArrayWriteBuffer();
162
+ buffer.onCommit(() => {
163
+ throw new Error('Max disconnected buffer size exceeded');
164
+ });
165
+ return buffer;
166
+ });
167
+
168
+ const protocol = new RpcProtocol(channel, undefined, { mode: 'clientOnly' });
169
+
170
+ // sendRequest should return a rejected promise, not throw synchronously
171
+ const promise = protocol.sendRequest('testMethod', []);
172
+
173
+ try {
174
+ await promise;
175
+ assert.fail('Expected promise to be rejected');
176
+ } catch (err) {
177
+ expect(err).to.be.instanceOf(Error);
178
+ expect((err as Error).message).to.contain('buffer size exceeded');
179
+ }
180
+ });
181
+
182
+ it('should not leak pending requests when commit fails', async () => {
183
+ const channel = new ForwardingChannel('test', () => { }, () => {
184
+ const buffer = new Uint8ArrayWriteBuffer();
185
+ buffer.onCommit(() => {
186
+ throw new Error('Max disconnected buffer size exceeded');
187
+ });
188
+ return buffer;
189
+ });
190
+
191
+ const protocol = new RpcProtocol(channel, undefined, { mode: 'clientOnly' });
192
+
193
+ // sendRequest should return a rejected promise and clean up pendingRequests
194
+ try {
195
+ await protocol.sendRequest('testMethod', []);
196
+ } catch {
197
+ // expected: the promise is rejected due to buffer overflow
198
+ }
199
+
200
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
+ expect((protocol as any).pendingRequests.size).to.equal(0);
202
+ });
87
203
  });
88
204
  });