@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.
- package/README.md +3 -3
- package/lib/browser/browser.d.ts +3 -0
- package/lib/browser/browser.d.ts.map +1 -1
- package/lib/browser/browser.js +51 -1
- package/lib/browser/browser.js.map +1 -1
- package/lib/browser/common-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/common-frontend-contribution.js +59 -10
- package/lib/browser/common-frontend-contribution.js.map +1 -1
- package/lib/browser/preferences/preference-validation-service.d.ts +13 -0
- package/lib/browser/preferences/preference-validation-service.d.ts.map +1 -1
- package/lib/browser/preferences/preference-validation-service.js +92 -7
- package/lib/browser/preferences/preference-validation-service.js.map +1 -1
- package/lib/browser/preferences/preference-validation-service.spec.js +58 -0
- package/lib/browser/preferences/preference-validation-service.spec.js.map +1 -1
- package/lib/browser/preloader.d.ts +2 -0
- package/lib/browser/preloader.d.ts.map +1 -0
- package/lib/browser/{nls-loader.js → preloader.js} +25 -6
- package/lib/browser/preloader.js.map +1 -0
- package/lib/browser/quick-input/quick-pick-service-impl.d.ts +1 -1
- package/lib/browser/quick-input/quick-pick-service-impl.d.ts.map +1 -1
- package/lib/browser/quick-input/quick-pick-service-impl.js.map +1 -1
- package/lib/browser/resource-context-key.js +2 -3
- package/lib/browser/resource-context-key.js.map +1 -1
- package/lib/browser/saveable.d.ts +6 -1
- package/lib/browser/saveable.d.ts.map +1 -1
- package/lib/browser/saveable.js.map +1 -1
- package/lib/browser/shell/application-shell.d.ts.map +1 -1
- package/lib/browser/shell/application-shell.js +1 -0
- package/lib/browser/shell/application-shell.js.map +1 -1
- package/lib/browser/status-bar/status-bar.d.ts +2 -0
- package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
- package/lib/browser/status-bar/status-bar.js +2 -1
- package/lib/browser/status-bar/status-bar.js.map +1 -1
- package/lib/browser/tree/tree-widget.d.ts +1 -1
- package/lib/browser/tree/tree-widget.d.ts.map +1 -1
- package/lib/browser/tree/tree-widget.js +18 -7
- package/lib/browser/tree/tree-widget.js.map +1 -1
- package/lib/browser/widget-decoration.d.ts +11 -0
- package/lib/browser/widget-decoration.d.ts.map +1 -1
- package/lib/browser/widget-decoration.js +7 -0
- package/lib/browser/widget-decoration.js.map +1 -1
- package/lib/browser/widgets/select-component.d.ts +52 -0
- package/lib/browser/widgets/select-component.d.ts.map +1 -0
- package/lib/browser/widgets/select-component.js +287 -0
- package/lib/browser/widgets/select-component.js.map +1 -0
- package/lib/common/application-protocol.d.ts +3 -0
- package/lib/common/application-protocol.d.ts.map +1 -1
- package/lib/common/disposable.d.ts +1 -0
- package/lib/common/disposable.d.ts.map +1 -1
- package/lib/common/disposable.js +12 -4
- package/lib/common/disposable.js.map +1 -1
- package/lib/common/disposable.spec.d.ts +2 -0
- package/lib/common/disposable.spec.d.ts.map +1 -0
- package/lib/common/disposable.spec.js +31 -0
- package/lib/common/disposable.spec.js.map +1 -0
- package/lib/common/i18n/localization.d.ts +7 -5
- package/lib/common/i18n/localization.d.ts.map +1 -1
- package/lib/common/i18n/localization.js.map +1 -1
- package/lib/common/json-schema.d.ts +1 -0
- package/lib/common/json-schema.d.ts.map +1 -1
- package/lib/common/nls.js +24 -4
- package/lib/common/nls.js.map +1 -1
- package/lib/common/os.d.ts +5 -0
- package/lib/common/os.d.ts.map +1 -1
- package/lib/common/os.js +5 -0
- package/lib/common/os.js.map +1 -1
- package/lib/common/path.d.ts +20 -17
- package/lib/common/path.d.ts.map +1 -1
- package/lib/common/path.js +37 -0
- package/lib/common/path.js.map +1 -1
- package/lib/common/path.spec.js +37 -0
- package/lib/common/path.spec.js.map +1 -1
- package/lib/common/quick-pick-service.d.ts +1 -1
- package/lib/common/quick-pick-service.d.ts.map +1 -1
- package/lib/common/quick-pick-service.js +0 -15
- package/lib/common/quick-pick-service.js.map +1 -1
- package/lib/common/selection-service.d.ts +7 -0
- package/lib/common/selection-service.d.ts.map +1 -1
- package/lib/common/selection-service.js +5 -0
- package/lib/common/selection-service.js.map +1 -1
- package/lib/common/uri.d.ts +12 -1
- package/lib/common/uri.d.ts.map +1 -1
- package/lib/common/uri.js +14 -0
- package/lib/common/uri.js.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.d.ts +1 -0
- package/lib/electron-browser/menu/electron-menu-contribution.d.ts.map +1 -1
- package/lib/electron-browser/menu/electron-menu-contribution.js +10 -4
- package/lib/electron-browser/menu/electron-menu-contribution.js.map +1 -1
- package/lib/electron-common/electron-token.d.ts +3 -3
- package/lib/electron-common/electron-token.d.ts.map +1 -1
- package/lib/electron-common/electron-token.js +4 -3
- package/lib/electron-common/electron-token.js.map +1 -1
- package/lib/node/backend-application-module.d.ts.map +1 -1
- package/lib/node/backend-application-module.js +3 -0
- package/lib/node/backend-application-module.js.map +1 -1
- package/lib/node/i18n/localization-backend-contribution.js +1 -1
- package/lib/node/i18n/localization-backend-contribution.js.map +1 -1
- package/lib/node/i18n/localization-contribution.d.ts +4 -3
- package/lib/node/i18n/localization-contribution.d.ts.map +1 -1
- package/lib/node/i18n/localization-contribution.js +15 -10
- package/lib/node/i18n/localization-contribution.js.map +1 -1
- package/lib/node/i18n/localization-provider.d.ts +2 -2
- package/lib/node/i18n/localization-provider.d.ts.map +1 -1
- package/lib/node/i18n/localization-provider.js +9 -4
- package/lib/node/i18n/localization-provider.js.map +1 -1
- package/lib/node/os-backend-application-contribution.d.ts +6 -0
- package/lib/node/os-backend-application-contribution.d.ts.map +1 -0
- package/lib/node/os-backend-application-contribution.js +38 -0
- package/lib/node/os-backend-application-contribution.js.map +1 -0
- package/package.json +5 -5
- package/src/browser/browser.ts +62 -0
- package/src/browser/common-frontend-contribution.ts +68 -10
- package/src/browser/preferences/preference-validation-service.spec.ts +59 -0
- package/src/browser/preferences/preference-validation-service.ts +85 -8
- package/src/browser/{nls-loader.ts → preloader.ts} +26 -4
- package/src/browser/quick-input/quick-pick-service-impl.ts +2 -2
- package/src/browser/resource-context-key.ts +2 -2
- package/src/browser/saveable.ts +3 -1
- package/src/browser/shell/application-shell.ts +1 -0
- package/src/browser/status-bar/status-bar.tsx +4 -1
- package/src/browser/style/select-component.css +96 -0
- package/src/browser/style/tree.css +6 -0
- package/src/browser/style/view-container.css +2 -2
- package/src/browser/tree/tree-widget.tsx +21 -9
- package/src/browser/widget-decoration.ts +14 -0
- package/src/browser/widgets/select-component.tsx +340 -0
- package/src/common/application-protocol.ts +3 -0
- package/src/common/disposable.spec.ts +30 -0
- package/src/common/disposable.ts +15 -4
- package/src/common/i18n/localization.ts +6 -3
- package/src/common/json-schema.ts +1 -0
- package/src/common/nls.ts +25 -4
- package/src/common/os.ts +6 -0
- package/src/common/path.spec.ts +44 -0
- package/src/common/path.ts +40 -0
- package/src/common/quick-pick-service.ts +1 -17
- package/src/common/selection-service.ts +8 -2
- package/src/common/uri.ts +26 -1
- package/src/electron-browser/menu/electron-menu-contribution.ts +11 -4
- package/src/electron-common/electron-token.ts +4 -3
- package/src/node/backend-application-module.ts +4 -0
- package/src/node/i18n/localization-backend-contribution.ts +1 -1
- package/src/node/i18n/localization-contribution.ts +21 -13
- package/src/node/i18n/localization-provider.ts +12 -7
- package/src/node/os-backend-application-contribution.ts +30 -0
- package/lib/browser/nls-loader.d.ts +0 -2
- package/lib/browser/nls-loader.d.ts.map +0 -1
- 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
|
+
});
|
package/src/common/disposable.ts
CHANGED
|
@@ -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
|
-
|
|
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<
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
113
|
+
list.push({
|
|
114
|
+
key: localizationKey,
|
|
115
|
+
message
|
|
116
|
+
});
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
|
-
return
|
|
119
|
+
return list;
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
private buildKey(key: string, filepath: string): string {
|
package/src/common/os.ts
CHANGED
package/src/common/path.spec.ts
CHANGED
|
@@ -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);
|
package/src/common/path.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|