@theia/core 1.25.0-next.9 → 1.25.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 (148) hide show
  1. package/README.md +3 -3
  2. package/lib/browser/browser.d.ts +3 -0
  3. package/lib/browser/browser.d.ts.map +1 -1
  4. package/lib/browser/browser.js +51 -1
  5. package/lib/browser/browser.js.map +1 -1
  6. package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
  7. package/lib/browser/common-frontend-contribution.js +59 -10
  8. package/lib/browser/common-frontend-contribution.js.map +1 -1
  9. package/lib/browser/preferences/preference-validation-service.d.ts +13 -0
  10. package/lib/browser/preferences/preference-validation-service.d.ts.map +1 -1
  11. package/lib/browser/preferences/preference-validation-service.js +92 -7
  12. package/lib/browser/preferences/preference-validation-service.js.map +1 -1
  13. package/lib/browser/preferences/preference-validation-service.spec.js +58 -0
  14. package/lib/browser/preferences/preference-validation-service.spec.js.map +1 -1
  15. package/lib/browser/preloader.d.ts +2 -0
  16. package/lib/browser/preloader.d.ts.map +1 -0
  17. package/lib/browser/{nls-loader.js → preloader.js} +25 -6
  18. package/lib/browser/preloader.js.map +1 -0
  19. package/lib/browser/quick-input/quick-pick-service-impl.d.ts +1 -1
  20. package/lib/browser/quick-input/quick-pick-service-impl.d.ts.map +1 -1
  21. package/lib/browser/quick-input/quick-pick-service-impl.js.map +1 -1
  22. package/lib/browser/resource-context-key.js +2 -3
  23. package/lib/browser/resource-context-key.js.map +1 -1
  24. package/lib/browser/saveable.d.ts +6 -1
  25. package/lib/browser/saveable.d.ts.map +1 -1
  26. package/lib/browser/saveable.js.map +1 -1
  27. package/lib/browser/shell/application-shell.d.ts.map +1 -1
  28. package/lib/browser/shell/application-shell.js +1 -0
  29. package/lib/browser/shell/application-shell.js.map +1 -1
  30. package/lib/browser/status-bar/status-bar.d.ts +2 -0
  31. package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
  32. package/lib/browser/status-bar/status-bar.js +2 -1
  33. package/lib/browser/status-bar/status-bar.js.map +1 -1
  34. package/lib/browser/tree/tree-widget.d.ts +1 -1
  35. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  36. package/lib/browser/tree/tree-widget.js +18 -7
  37. package/lib/browser/tree/tree-widget.js.map +1 -1
  38. package/lib/browser/widget-decoration.d.ts +11 -0
  39. package/lib/browser/widget-decoration.d.ts.map +1 -1
  40. package/lib/browser/widget-decoration.js +7 -0
  41. package/lib/browser/widget-decoration.js.map +1 -1
  42. package/lib/browser/widgets/select-component.d.ts +52 -0
  43. package/lib/browser/widgets/select-component.d.ts.map +1 -0
  44. package/lib/browser/widgets/select-component.js +287 -0
  45. package/lib/browser/widgets/select-component.js.map +1 -0
  46. package/lib/common/application-protocol.d.ts +3 -0
  47. package/lib/common/application-protocol.d.ts.map +1 -1
  48. package/lib/common/disposable.d.ts +1 -0
  49. package/lib/common/disposable.d.ts.map +1 -1
  50. package/lib/common/disposable.js +12 -4
  51. package/lib/common/disposable.js.map +1 -1
  52. package/lib/common/disposable.spec.d.ts +2 -0
  53. package/lib/common/disposable.spec.d.ts.map +1 -0
  54. package/lib/common/disposable.spec.js +31 -0
  55. package/lib/common/disposable.spec.js.map +1 -0
  56. package/lib/common/i18n/localization.d.ts +7 -5
  57. package/lib/common/i18n/localization.d.ts.map +1 -1
  58. package/lib/common/i18n/localization.js.map +1 -1
  59. package/lib/common/json-schema.d.ts +1 -0
  60. package/lib/common/json-schema.d.ts.map +1 -1
  61. package/lib/common/nls.js +24 -4
  62. package/lib/common/nls.js.map +1 -1
  63. package/lib/common/os.d.ts +5 -0
  64. package/lib/common/os.d.ts.map +1 -1
  65. package/lib/common/os.js +5 -0
  66. package/lib/common/os.js.map +1 -1
  67. package/lib/common/path.d.ts +20 -17
  68. package/lib/common/path.d.ts.map +1 -1
  69. package/lib/common/path.js +37 -0
  70. package/lib/common/path.js.map +1 -1
  71. package/lib/common/path.spec.js +37 -0
  72. package/lib/common/path.spec.js.map +1 -1
  73. package/lib/common/quick-pick-service.d.ts +1 -1
  74. package/lib/common/quick-pick-service.d.ts.map +1 -1
  75. package/lib/common/quick-pick-service.js +0 -15
  76. package/lib/common/quick-pick-service.js.map +1 -1
  77. package/lib/common/selection-service.d.ts +7 -0
  78. package/lib/common/selection-service.d.ts.map +1 -1
  79. package/lib/common/selection-service.js +5 -0
  80. package/lib/common/selection-service.js.map +1 -1
  81. package/lib/common/uri.d.ts +12 -1
  82. package/lib/common/uri.d.ts.map +1 -1
  83. package/lib/common/uri.js +14 -0
  84. package/lib/common/uri.js.map +1 -1
  85. package/lib/electron-browser/menu/electron-menu-contribution.d.ts +1 -0
  86. package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
  87. package/lib/electron-browser/menu/electron-menu-contribution.js +10 -4
  88. package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
  89. package/lib/electron-common/electron-token.d.ts +3 -3
  90. package/lib/electron-common/electron-token.d.ts.map +1 -1
  91. package/lib/electron-common/electron-token.js +4 -3
  92. package/lib/electron-common/electron-token.js.map +1 -1
  93. package/lib/node/backend-application-module.d.ts.map +1 -1
  94. package/lib/node/backend-application-module.js +3 -0
  95. package/lib/node/backend-application-module.js.map +1 -1
  96. package/lib/node/i18n/localization-backend-contribution.js +1 -1
  97. package/lib/node/i18n/localization-backend-contribution.js.map +1 -1
  98. package/lib/node/i18n/localization-contribution.d.ts +4 -3
  99. package/lib/node/i18n/localization-contribution.d.ts.map +1 -1
  100. package/lib/node/i18n/localization-contribution.js +15 -10
  101. package/lib/node/i18n/localization-contribution.js.map +1 -1
  102. package/lib/node/i18n/localization-provider.d.ts +2 -2
  103. package/lib/node/i18n/localization-provider.d.ts.map +1 -1
  104. package/lib/node/i18n/localization-provider.js +9 -4
  105. package/lib/node/i18n/localization-provider.js.map +1 -1
  106. package/lib/node/os-backend-application-contribution.d.ts +6 -0
  107. package/lib/node/os-backend-application-contribution.d.ts.map +1 -0
  108. package/lib/node/os-backend-application-contribution.js +38 -0
  109. package/lib/node/os-backend-application-contribution.js.map +1 -0
  110. package/package.json +5 -5
  111. package/src/browser/browser.ts +62 -0
  112. package/src/browser/common-frontend-contribution.ts +68 -10
  113. package/src/browser/preferences/preference-validation-service.spec.ts +59 -0
  114. package/src/browser/preferences/preference-validation-service.ts +85 -8
  115. package/src/browser/{nls-loader.ts → preloader.ts} +26 -4
  116. package/src/browser/quick-input/quick-pick-service-impl.ts +2 -2
  117. package/src/browser/resource-context-key.ts +2 -2
  118. package/src/browser/saveable.ts +3 -1
  119. package/src/browser/shell/application-shell.ts +1 -0
  120. package/src/browser/status-bar/status-bar.tsx +4 -1
  121. package/src/browser/style/select-component.css +96 -0
  122. package/src/browser/style/tree.css +6 -0
  123. package/src/browser/style/view-container.css +2 -2
  124. package/src/browser/tree/tree-widget.tsx +21 -9
  125. package/src/browser/widget-decoration.ts +14 -0
  126. package/src/browser/widgets/select-component.tsx +340 -0
  127. package/src/common/application-protocol.ts +3 -0
  128. package/src/common/disposable.spec.ts +30 -0
  129. package/src/common/disposable.ts +15 -4
  130. package/src/common/i18n/localization.ts +6 -3
  131. package/src/common/json-schema.ts +1 -0
  132. package/src/common/nls.ts +25 -4
  133. package/src/common/os.ts +6 -0
  134. package/src/common/path.spec.ts +44 -0
  135. package/src/common/path.ts +40 -0
  136. package/src/common/quick-pick-service.ts +1 -17
  137. package/src/common/selection-service.ts +8 -2
  138. package/src/common/uri.ts +26 -1
  139. package/src/electron-browser/menu/electron-menu-contribution.ts +11 -4
  140. package/src/electron-common/electron-token.ts +4 -3
  141. package/src/node/backend-application-module.ts +4 -0
  142. package/src/node/i18n/localization-backend-contribution.ts +1 -1
  143. package/src/node/i18n/localization-contribution.ts +21 -13
  144. package/src/node/i18n/localization-provider.ts +12 -7
  145. package/src/node/os-backend-application-contribution.ts +30 -0
  146. package/lib/browser/nls-loader.d.ts +0 -2
  147. package/lib/browser/nls-loader.d.ts.map +0 -1
  148. package/lib/browser/nls-loader.js.map +0 -1
@@ -0,0 +1,340 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 TypeFox 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 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as React from 'react';
18
+ import * as ReactDOM from 'react-dom';
19
+ import * as DOMPurify from 'dompurify';
20
+ import { codicon } from './widget';
21
+ import { measureTextHeight, measureTextWidth } from '../browser';
22
+
23
+ import '../../../src/browser/style/select-component.css';
24
+
25
+ export interface SelectOption {
26
+ value?: string
27
+ label?: string
28
+ separator?: boolean
29
+ disabled?: boolean
30
+ detail?: string
31
+ description?: string
32
+ markdown?: boolean
33
+ }
34
+
35
+ export interface SelectComponentProps {
36
+ options: SelectOption[]
37
+ value?: string | number
38
+ onChange?: (option: SelectOption, index: number) => void
39
+ }
40
+
41
+ export interface SelectComponentDropdownDimensions {
42
+ top: number
43
+ left: number
44
+ width: number
45
+ parentHeight: number
46
+ };
47
+
48
+ export interface SelectComponentState {
49
+ dimensions?: SelectComponentDropdownDimensions
50
+ selected: number
51
+ original: number
52
+ hover: number
53
+ }
54
+
55
+ export const SELECT_COMPONENT_CONTAINER = 'select-component-container';
56
+
57
+ export class SelectComponent extends React.Component<SelectComponentProps, SelectComponentState> {
58
+ protected dropdownElement: HTMLElement;
59
+ protected fieldRef = React.createRef<HTMLDivElement>();
60
+ protected mountedListeners: Map<string, EventListenerOrEventListenerObject> = new Map();
61
+ protected optimalWidth = 0;
62
+ protected optimalHeight = 0;
63
+
64
+ constructor(props: SelectComponentProps) {
65
+ super(props);
66
+ let selected = 0;
67
+ if (typeof props.value === 'number') {
68
+ selected = props.value;
69
+ } else if (typeof props.value === 'string') {
70
+ selected = Math.max(props.options.findIndex(e => e.value === props.value), 0);
71
+ }
72
+ this.state = {
73
+ selected,
74
+ original: selected,
75
+ hover: selected
76
+ };
77
+
78
+ let list = document.getElementById(SELECT_COMPONENT_CONTAINER);
79
+ if (!list) {
80
+ list = document.createElement('div');
81
+ list.id = SELECT_COMPONENT_CONTAINER;
82
+ document.body.appendChild(list);
83
+ }
84
+ this.dropdownElement = list;
85
+ }
86
+
87
+ get value(): string | number | undefined {
88
+ return this.props.options[this.state.selected].value ?? this.state.selected;
89
+ }
90
+
91
+ set value(value: string | number | undefined) {
92
+ let index = -1;
93
+ if (typeof value === 'number') {
94
+ index = value;
95
+ } else if (typeof value === 'string') {
96
+ index = this.props.options.findIndex(e => e.value === value);
97
+ }
98
+ if (index >= 0) {
99
+ this.setState({
100
+ selected: index,
101
+ original: index,
102
+ hover: index
103
+ });
104
+ }
105
+ }
106
+
107
+ protected getOptimalWidth(): number {
108
+ const textWidth = measureTextWidth(this.props.options.map(e => e.label || e.value || '' + (e.detail || '')));
109
+ return Math.ceil(textWidth + 16);
110
+ }
111
+
112
+ protected getOptimalHeight(maxWidth?: number): number {
113
+ const firstLine = this.props.options.find(e => e.label || e.value || e.detail);
114
+ if (!firstLine) {
115
+ return 0;
116
+ }
117
+ if (maxWidth) {
118
+ maxWidth = Math.ceil(maxWidth) + 10; // Increase width by 10 due to side padding
119
+ }
120
+ const descriptionHeight = measureTextHeight(this.props.options.map(e => e.description || ''), { maxWidth: `${maxWidth}px` }) + 18;
121
+ const singleLineHeight = measureTextHeight(firstLine.label || firstLine.value || firstLine.detail || '') + 6;
122
+ const optimal = descriptionHeight + singleLineHeight * this.props.options.length;
123
+ return optimal + 20; // Just to be safe, add another 20 pixels here
124
+ }
125
+
126
+ protected attachListeners(): void {
127
+ const hide = () => {
128
+ this.hide();
129
+ };
130
+ this.mountedListeners.set('scroll', hide);
131
+ this.mountedListeners.set('wheel', hide);
132
+
133
+ let parent = this.fieldRef.current?.parentElement;
134
+ while (parent) {
135
+ // Workaround for perfect scrollbar, since using `overflow: hidden`
136
+ // neither triggers the `scroll`, `wheel` nor `blur` event
137
+ if (parent.classList.contains('ps')) {
138
+ parent.addEventListener('ps-scroll-y', hide);
139
+ }
140
+ parent = parent.parentElement;
141
+ }
142
+
143
+ for (const [key, listener] of this.mountedListeners.entries()) {
144
+ window.addEventListener(key, listener);
145
+ }
146
+ }
147
+
148
+ override componentWillUnmount(): void {
149
+ if (this.mountedListeners.size > 0) {
150
+ const eventListener = this.mountedListeners.get('scroll')!;
151
+ let parent = this.fieldRef.current?.parentElement;
152
+ while (parent) {
153
+ parent.removeEventListener('ps-scroll-y', eventListener);
154
+ parent = parent.parentElement;
155
+ }
156
+ for (const [key, listener] of this.mountedListeners.entries()) {
157
+ window.removeEventListener(key, listener);
158
+ }
159
+ }
160
+ }
161
+
162
+ override render(): React.ReactNode {
163
+ const { options } = this.props;
164
+ let { selected } = this.state;
165
+ while (options[selected]?.separator) {
166
+ selected = (selected + 1) % this.props.options.length;
167
+ }
168
+ const selectedItemLabel = options[selected].label ?? options[selected].value;
169
+ return <>
170
+ <div
171
+ key="select-component"
172
+ ref={this.fieldRef}
173
+ tabIndex={0}
174
+ className="theia-select-component"
175
+ onClick={e => this.handleClickEvent(e)}
176
+ onBlur={() => this.hide()}
177
+ onKeyDown={e => this.handleKeypress(e)}
178
+ >
179
+ <div key="label" className="theia-select-component-label">{selectedItemLabel}</div>
180
+ <div key="icon" className={`theia-select-component-chevron ${codicon('chevron-down')}`} />
181
+ </div>
182
+ {ReactDOM.createPortal(this.renderDropdown(), this.dropdownElement)}
183
+ </>;
184
+ }
185
+
186
+ protected handleKeypress(ev: React.KeyboardEvent<HTMLDivElement>): void {
187
+ if (!this.fieldRef.current) {
188
+ return;
189
+ }
190
+ if (ev.key === 'ArrowUp') {
191
+ let selected = this.state.selected;
192
+ if (selected <= 0) {
193
+ selected = this.props.options.length - 1;
194
+ } else {
195
+ selected--;
196
+ }
197
+ this.setState({
198
+ selected,
199
+ hover: selected
200
+ });
201
+ } else if (ev.key === 'ArrowDown') {
202
+ if (this.state.dimensions) {
203
+ const selected = (this.state.selected + 1) % this.props.options.length;
204
+ this.setState({
205
+ selected,
206
+ hover: selected
207
+ });
208
+ } else {
209
+ this.toggleVisibility();
210
+ this.setState({
211
+ hover: 0,
212
+ selected: 0
213
+ });
214
+ }
215
+ } else if (ev.key === 'Enter') {
216
+ if (!this.state.dimensions) {
217
+ this.toggleVisibility();
218
+ } else {
219
+ const selected = this.state.selected;
220
+ this.selectOption(selected, this.props.options[selected]);
221
+ }
222
+ } else if (ev.key === 'Escape' || ev.key === 'Tab') {
223
+ this.hide();
224
+ }
225
+ ev.stopPropagation();
226
+ ev.nativeEvent.stopImmediatePropagation();
227
+ }
228
+
229
+ protected handleClickEvent(event: React.MouseEvent<HTMLElement>): void {
230
+ this.toggleVisibility();
231
+ event.stopPropagation();
232
+ event.nativeEvent.stopImmediatePropagation();
233
+ }
234
+
235
+ protected toggleVisibility(): void {
236
+ if (!this.fieldRef.current) {
237
+ return;
238
+ }
239
+ if (!this.state.dimensions) {
240
+ const rect = this.fieldRef.current.getBoundingClientRect();
241
+ this.setState({
242
+ dimensions: {
243
+ top: rect.top + rect.height,
244
+ left: rect.left,
245
+ width: rect.width,
246
+ parentHeight: rect.height
247
+ },
248
+ });
249
+ } else {
250
+ this.hide();
251
+ }
252
+ }
253
+
254
+ protected hide(index?: number): void {
255
+ const selectedIndex = index === undefined ? this.state.original : index;
256
+ this.setState({
257
+ dimensions: undefined,
258
+ selected: selectedIndex,
259
+ original: selectedIndex,
260
+ hover: selectedIndex
261
+ });
262
+ }
263
+
264
+ protected renderDropdown(): React.ReactNode {
265
+ if (!this.state.dimensions) {
266
+ return;
267
+ }
268
+ if (this.mountedListeners.size === 0) {
269
+ // Only attach our listeners once we render our dropdown menu
270
+ this.attachListeners();
271
+ // We can now also calculate the optimal width
272
+ this.optimalWidth = this.getOptimalWidth();
273
+ this.optimalHeight = this.getOptimalHeight(Math.max(this.state.dimensions.width, this.optimalWidth));
274
+ }
275
+ const clientRect = document.getElementById('theia-app-shell')!.getBoundingClientRect();
276
+ const invert = this.optimalHeight > clientRect.height - this.state.dimensions.top;
277
+ const { options } = this.props;
278
+ const { hover } = this.state;
279
+ const description = options[hover].description;
280
+ const markdown = options[hover].markdown;
281
+ const items = options.map((item, i) => this.renderOption(i, item));
282
+ if (description) {
283
+ let descriptionNode: React.ReactNode | undefined;
284
+ const className = 'theia-select-component-description';
285
+ if (markdown) {
286
+ descriptionNode = <div key="description" className={className}
287
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(description) }} />; // eslint-disable-line react/no-danger
288
+ } else {
289
+ descriptionNode = <div key="description" className={className}>
290
+ {description}
291
+ </div>;
292
+ }
293
+ if (invert) {
294
+ items.unshift(descriptionNode);
295
+ } else {
296
+ items.push(descriptionNode);
297
+ }
298
+ }
299
+ const calculatedWidth = Math.max(this.state.dimensions.width, this.optimalWidth);
300
+ const maxWidth = clientRect.width - this.state.dimensions.left;
301
+ return <div key="dropdown" className="theia-select-component-dropdown" style={{
302
+ top: invert ? 'none' : this.state.dimensions.top,
303
+ bottom: invert ? clientRect.height - this.state.dimensions.top + this.state.dimensions.parentHeight : 'none',
304
+ left: this.state.dimensions.left,
305
+ width: Math.min(calculatedWidth, maxWidth),
306
+ position: 'absolute'
307
+ }}>
308
+ {items}
309
+ </div>;
310
+ }
311
+
312
+ protected renderOption(index: number, option: SelectOption): React.ReactNode {
313
+ if (option.separator) {
314
+ return <div key={index} className="theia-select-component-separator" />;
315
+ }
316
+ const selected = this.state.hover;
317
+ return (
318
+ <div
319
+ key={index}
320
+ className={`theia-select-component-option${index === selected ? ' selected' : ''}`}
321
+ onMouseOver={() => {
322
+ this.setState({
323
+ hover: index
324
+ });
325
+ }}
326
+ onMouseDown={() => {
327
+ this.selectOption(index, option);
328
+ }}
329
+ >
330
+ <div key="value" className="theia-select-component-option-value">{option.label ?? option.value}</div>
331
+ {option.detail && <div key="detail" className="theia-select-component-option-detail">{option.detail}</div>}
332
+ </div>
333
+ );
334
+ }
335
+
336
+ protected selectOption(index: number, option: SelectOption): void {
337
+ this.props.onChange?.(option, index);
338
+ this.hide(index);
339
+ }
340
+ }
@@ -23,6 +23,9 @@ export const ApplicationServer = Symbol('ApplicationServer');
23
23
  export interface ApplicationServer {
24
24
  getExtensionsInfos(): Promise<ExtensionInfo[]>;
25
25
  getApplicationInfo(): Promise<ApplicationInfo | undefined>;
26
+ /**
27
+ * @deprecated since 1.25.0. Use `OS.backend.type()` instead.
28
+ */
26
29
  getBackendOS(): Promise<OS.Type>;
27
30
  }
28
31
 
@@ -0,0 +1,30 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 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 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { DisposableCollection, Disposable } from './disposable';
19
+
20
+ describe('Disposables', () => {
21
+ /* eslint-disable no-unused-expressions */
22
+ it('Is safe to use Disposable.NULL', () => {
23
+ const collectionA = new DisposableCollection(Disposable.NULL);
24
+ const collectionB = new DisposableCollection(Disposable.NULL);
25
+ expect(!collectionA.disposed && !collectionB.disposed, 'Neither should be disposed before either is disposed.').to.be.true;
26
+ collectionA.dispose();
27
+ expect(collectionA.disposed, 'A should be disposed after being disposed.').to.be.true;
28
+ expect(collectionB.disposed, 'B should not be disposed because A was disposed.').to.be.false;
29
+ });
30
+ });
@@ -28,13 +28,24 @@ export namespace Disposable {
28
28
  return !!arg && typeof arg === 'object' && 'dispose' in arg && typeof arg['dispose'] === 'function';
29
29
  }
30
30
  export function create(func: () => void): Disposable {
31
- return {
32
- dispose: func
33
- };
31
+ return { dispose: func };
34
32
  }
35
- export const NULL = create(() => { });
33
+ /** Always provides a reference to a new disposable. */
34
+ export declare const NULL: Disposable;
36
35
  }
37
36
 
37
+ /**
38
+ * Ensures that every reference to {@link Disposable.NULL} returns a new object,
39
+ * as sharing a disposable between multiple {@link DisposableCollection} can have unexpected side effects
40
+ */
41
+ Object.defineProperty(Disposable, 'NULL', {
42
+ configurable: false,
43
+ enumerable: true,
44
+ get(): Disposable {
45
+ return { dispose: () => { } };
46
+ }
47
+ });
48
+
38
49
  export class DisposableCollection implements Disposable {
39
50
 
40
51
  protected readonly disposables: Disposable[] = [];
@@ -20,16 +20,19 @@ export const AsyncLocalizationProvider = Symbol('AsyncLocalizationProvider');
20
20
  export interface AsyncLocalizationProvider {
21
21
  getCurrentLanguage(): Promise<string>
22
22
  setCurrentLanguage(languageId: string): Promise<void>
23
- getAvailableLanguages(): Promise<string[]>
23
+ getAvailableLanguages(): Promise<LanguageInfo[]>
24
24
  loadLocalization(languageId: string): Promise<Localization>
25
25
  }
26
26
 
27
- export interface Localization {
27
+ export interface Localization extends LanguageInfo {
28
+ translations: { [key: string]: string };
29
+ }
30
+
31
+ export interface LanguageInfo {
28
32
  languageId: string;
29
33
  languageName?: string;
30
34
  languagePack?: boolean;
31
35
  localizedLanguageName?: string;
32
- translations: { [key: string]: string };
33
36
  }
34
37
 
35
38
  export type FormatType = string | number | boolean | undefined;
@@ -47,6 +47,7 @@ export interface IJSONSchema {
47
47
  maxProperties?: number;
48
48
  dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
49
49
  items?: IJSONSchema | IJSONSchema[];
50
+ prefixItems?: IJSONSchema[];
50
51
  minItems?: number;
51
52
  maxItems?: number;
52
53
  uniqueItems?: boolean;
package/src/common/nls.ts CHANGED
@@ -71,7 +71,8 @@ class LocalizationKeyProvider {
71
71
  private data = this.buildData();
72
72
 
73
73
  get(defaultValue: string): string | undefined {
74
- return this.data.get(Localization.normalize(defaultValue.toLowerCase()));
74
+ const normalized = Localization.normalize(defaultValue);
75
+ return this.data.get(normalized) || this.data.get(normalized.toUpperCase());
75
76
  }
76
77
 
77
78
  /**
@@ -86,16 +87,36 @@ class LocalizationKeyProvider {
86
87
  const keys: NlsKeys = bundles.keys;
87
88
  const messages: Record<string, string[]> = bundles.messages;
88
89
  const data = new Map<string, string>();
90
+ const keysAndMessages = this.buildKeyMessageTuples(keys, messages);
91
+ for (const { key, message } of keysAndMessages) {
92
+ data.set(message, key);
93
+ }
94
+ // Second pass adds each message again in upper case, if the message doesn't already exist in upper case
95
+ // The second pass is needed to not accidentally override any translations which actually use the upper case message
96
+ for (const { key, message } of keysAndMessages) {
97
+ const upperMessage = message.toUpperCase();
98
+ if (!data.has(upperMessage)) {
99
+ data.set(upperMessage, key);
100
+ }
101
+ }
102
+ return data;
103
+ }
104
+
105
+ private buildKeyMessageTuples(keys: NlsKeys, messages: Record<string, string[]>): { key: string, message: string }[] {
106
+ const list: { key: string, message: string }[] = [];
89
107
  for (const [fileKey, messageBundle] of Object.entries(messages)) {
90
108
  const keyBundle = keys[fileKey];
91
109
  for (let i = 0; i < messageBundle.length; i++) {
92
- const message = Localization.normalize(messageBundle[i]).toLowerCase();
110
+ const message = Localization.normalize(messageBundle[i]);
93
111
  const key = keyBundle[i];
94
112
  const localizationKey = this.buildKey(typeof key === 'string' ? key : key.key, fileKey);
95
- data.set(message, localizationKey);
113
+ list.push({
114
+ key: localizationKey,
115
+ message
116
+ });
96
117
  }
97
118
  }
98
- return data;
119
+ return list;
99
120
  }
100
121
 
101
122
  private buildKey(key: string, filepath: string): string {
package/src/common/os.ts CHANGED
@@ -62,4 +62,10 @@ export namespace OS {
62
62
  return Type.Linux;
63
63
  }
64
64
 
65
+ export const backend = {
66
+ type,
67
+ isWindows,
68
+ isOSX
69
+ };
70
+
65
71
  }
@@ -357,6 +357,50 @@ describe('Path', () => {
357
357
 
358
358
  });
359
359
 
360
+ describe('fsPath#windows', () => {
361
+ it('should retain windows style path', () => {
362
+ const path = 'C:\\path\\to\\file.txt';
363
+ expect(new Path(path).fsPath(Path.Format.Windows)).eq(path);
364
+ });
365
+
366
+ it('should create windows style path with slashes', () => {
367
+ const path = 'C:/path/to/file.txt';
368
+ const expected = 'C:\\path\\to\\file.txt';
369
+ expect(new Path(path).fsPath(Path.Format.Windows)).eq(expected);
370
+ });
371
+
372
+ it('should append slashes to drive letter', () => {
373
+ const path = 'C:';
374
+ const expected = 'C:\\';
375
+ expect(new Path(path).fsPath(Path.Format.Windows)).eq(expected);
376
+ });
377
+
378
+ it('should create windows style path from posix', () => {
379
+ const path = '/path/to/file.txt';
380
+ const expected = '\\path\\to\\file.txt';
381
+ expect(new Path(path).fsPath(Path.Format.Windows)).eq(expected);
382
+ });
383
+ });
384
+
385
+ describe('fsPath#posix', () => {
386
+ it('should retain posix style path', () => {
387
+ const path = '/path/to/file.txt';
388
+ expect(new Path(path).fsPath(Path.Format.Posix)).eq(path);
389
+ });
390
+
391
+ it('should create posix style path from windows with slashes', () => {
392
+ const path = 'C:/path/to/file.txt';
393
+ const expected = '/c:/path/to/file.txt';
394
+ expect(new Path(path).fsPath(Path.Format.Posix)).eq(expected);
395
+ });
396
+
397
+ it('should create posix style path from windows', () => {
398
+ const path = 'C:\\path\\to\\file.txt';
399
+ const expected = '/c:/path/to/file.txt';
400
+ expect(new Path(path).fsPath(Path.Format.Posix)).eq(expected);
401
+ });
402
+ });
403
+
360
404
  function checkResolution(original: string, segments: string[], expected: string | undefined): void {
361
405
  it(`should resolve ${original} and ${segments.join(', ')} to ${expected}`, () => {
362
406
  const start = new Path(original);
@@ -31,6 +31,9 @@
31
31
  * " /c: / home/user/dir / file .txt "
32
32
  * └──────┴───────────────┴──────┴─────┘
33
33
  */
34
+
35
+ import { OS } from './os';
36
+
34
37
  export class Path {
35
38
  static separator: '/' = '/';
36
39
 
@@ -70,6 +73,22 @@ export class Path {
70
73
  return path.split(/[\\]/).join(Path.separator);
71
74
  }
72
75
 
76
+ /**
77
+ * Creates a windows path from the given path string.
78
+ * A windows path uses an upper case drive letter and backwards slashes.
79
+ * @param path The input path
80
+ * @returns Windows style path
81
+ */
82
+ static windowsPath(path: string): string {
83
+ const offset = path.charAt(0) === '/' ? 1 : 0;
84
+ if (path.charAt(offset + 1) === ':') {
85
+ const driveLetter = path.charAt(offset).toUpperCase();
86
+ const substring = path.substring(offset + 2).replace(/\//g, '\\');
87
+ return `${driveLetter}:${substring || '\\'}`;
88
+ }
89
+ return path.replace(/\//g, '\\');
90
+ }
91
+
73
92
  /**
74
93
  * Tildify path, replacing `home` with `~` if user's `home` is present at the beginning of the path.
75
94
  * This is a non-operation for Windows.
@@ -230,6 +249,20 @@ export class Path {
230
249
  return this.raw;
231
250
  }
232
251
 
252
+ /**
253
+ * Converts the current path into a file system path.
254
+ * @param format Determines the format of the path.
255
+ * If `undefined`, the format will be determined by the `OS.backend.type` value.
256
+ * @returns A file system path.
257
+ */
258
+ fsPath(format?: Path.Format): string {
259
+ if (format === Path.Format.Windows || (format === undefined && OS.backend.isWindows)) {
260
+ return Path.windowsPath(this.raw);
261
+ } else {
262
+ return this.raw;
263
+ }
264
+ }
265
+
233
266
  relative(path: Path): Path | undefined {
234
267
  if (this.raw === path.raw) {
235
268
  return new Path('');
@@ -292,3 +325,10 @@ export class Path {
292
325
  return new Path((this.isAbsolute ? '/' : '') + resultArray.join('/') + (trailingSlash ? '/' : ''));
293
326
  }
294
327
  }
328
+
329
+ export namespace Path {
330
+ export enum Format {
331
+ Posix,
332
+ Windows
333
+ }
334
+ }
@@ -14,22 +14,6 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- // *****************************************************************************
18
- // Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
19
- //
20
- // This program and the accompanying materials are made available under the
21
- // terms of the Eclipse Public License v. 2.0 which is available at
22
- // http://www.eclipse.org/legal/epl-2.0.
23
- //
24
- // This Source Code may also be made available under the following Secondary
25
- // Licenses when the conditions for such availability set forth in the Eclipse
26
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
27
- // with the GNU Classpath Exception which is available at
28
- // https://www.gnu.org/software/classpath/license.html.
29
- //
30
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
31
- // *****************************************************************************
32
-
33
17
  import URI from './uri';
34
18
  import * as fuzzy from 'fuzzy';
35
19
  import { Event } from './event';
@@ -295,7 +279,7 @@ export interface QuickInputService {
295
279
  input(options?: InputOptions, token?: CancellationToken): Promise<string | undefined>;
296
280
  pick<T extends QuickPickItem, O extends PickOptions<T>>(picks: Promise<T[]> | T[], options?: O, token?: CancellationToken):
297
281
  Promise<(O extends { canPickMany: true } ? T[] : T) | undefined>;
298
- showQuickPick<T extends QuickPickItem>(items: Array<T | QuickPickSeparator>, options?: QuickPickOptions<T>): Promise<T>;
282
+ showQuickPick<T extends QuickPickItem>(items: Array<T | QuickPickSeparator>, options?: QuickPickOptions<T>): Promise<T | undefined>;
299
283
  hide(): void;
300
284
  /**
301
285
  * Provides raw access to the quick pick controller.
@@ -13,16 +13,22 @@
13
13
  //
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
17
 
17
18
  import { injectable } from 'inversify';
18
19
  import { Emitter, Event } from '../common/event';
19
20
 
20
- /* eslint-disable @typescript-eslint/no-explicit-any */
21
-
21
+ /**
22
+ * `SelectionProvider` is implemented by services to notify listeners about selection changes.
23
+ */
22
24
  export interface SelectionProvider<T> {
23
25
  onSelectionChanged: Event<T | undefined>;
24
26
  }
25
27
 
28
+ /**
29
+ * Singleton service that is used to share the current selection globally in a Theia application.
30
+ * On each change of selection, subscribers are notified and receive the updated selection object.
31
+ */
26
32
  @injectable()
27
33
  export class SelectionService implements SelectionProvider<Object | undefined> {
28
34