Skip to content

Commit 8ac9009

Browse files
author
Akos Kitta
committed
fix: debug widget layout updates
Use customized `PanelLayout#removeWidget` and `PanelLayout#insertWidget` logic for the layout updates. The customized functions ensure no side effect if adding/removing the widget to/from the layout but it's already present/absent. Unlike the default [`PanelLayout#removeWidget`](https://github.com/phosphorjs/phosphor/blob/9f5e11025b62d2c4a6fb59e2681ae1ed323dcde4/packages/widgets/src/panellayout.ts#L154-L156) behavior, do not try to remove the widget if it's not present (has a negative index). Otherwise, required widgets might be removed based on the default [`ArrayExt#removeAt`](https://github.com/phosphorjs/phosphor/blob/9f5e11025b62d2c4a6fb59e2681ae1ed323dcde4/packages/algorithm/src/array.ts#L1075-L1077) behavior. Closes: #2354 Signed-off-by: Akos Kitta <[email protected]>
1 parent 74c5801 commit 8ac9009

File tree

3 files changed

+179
-11
lines changed

3 files changed

+179
-11
lines changed

arduino-ide-extension/src/browser/theia/debug/debug-widget.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import {
2-
codicon,
3-
PanelLayout,
4-
Widget,
5-
} from '@theia/core/lib/browser/widgets/widget';
1+
import { codicon } from '@theia/core/lib/browser/widgets/widget';
2+
import { Widget } from '@theia/core/shared/@phosphor/widgets';
63
import {
74
inject,
85
injectable,
96
postConstruct,
107
} from '@theia/core/shared/inversify';
118
import { DebugWidget as TheiaDebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
129
import { DebugDisabledStatusMessageSource } from '../../contributions/debug';
10+
import {
11+
removeWidgetIfPresent,
12+
unshiftWidgetIfNotPresent,
13+
} from '../dialogs/widgets';
1314

1415
@injectable()
1516
export class DebugWidget extends TheiaDebugWidget {
@@ -38,12 +39,10 @@ export class DebugWidget extends TheiaDebugWidget {
3839
this.messageNode.textContent = message ?? '';
3940
const enabled = !message;
4041
updateVisibility(enabled, this.toolbar, this.sessionWidget);
41-
if (this.layout instanceof PanelLayout) {
42-
if (enabled) {
43-
this.layout.removeWidget(this.statusMessageWidget);
44-
} else {
45-
this.layout.insertWidget(0, this.statusMessageWidget);
46-
}
42+
if (enabled) {
43+
removeWidgetIfPresent(this.layout, this.statusMessageWidget);
44+
} else {
45+
unshiftWidgetIfNotPresent(this.layout, this.statusMessageWidget);
4746
}
4847
this.title.iconClass = enabled ? codicon('debug-alt') : 'fa fa-ban'; // TODO: find a better icon?
4948
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
Layout,
3+
PanelLayout,
4+
Widget,
5+
} from '@theia/core/shared/@phosphor/widgets';
6+
7+
/**
8+
*
9+
* Removes the widget from the layout if the `layout` is a `PanelLayout` and the widget is present in the layout.
10+
* Otherwise, it's NOOP
11+
* @param layout the layout to remove the widget from. Must be a `PanelLayout`.
12+
* @param toRemove the widget to remove from the layout
13+
*/
14+
export function removeWidgetIfPresent(
15+
layout: Layout | null,
16+
toRemove: Widget
17+
): void {
18+
if (layout instanceof PanelLayout) {
19+
const index = layout.widgets.indexOf(toRemove);
20+
if (index < 0) {
21+
// Unlike the default `PanelLayout#removeWidget` behavior, (https://github.com/phosphorjs/phosphor/blob/9f5e11025b62d2c4a6fb59e2681ae1ed323dcde4/packages/widgets/src/panellayout.ts#L154-L156)
22+
// do not try to remove widget if it's not present (the index is negative).
23+
// Otherwise, required widgets could be removed based on the default ArrayExt behavior (https://github.com/phosphorjs/phosphor/blob/9f5e11025b62d2c4a6fb59e2681ae1ed323dcde4/packages/algorithm/src/array.ts#L1075-L1077)
24+
// See https://github.com/arduino/arduino-ide/issues/2354 for more details.
25+
return;
26+
}
27+
layout.removeWidget(toRemove);
28+
}
29+
}
30+
31+
/**
32+
*
33+
* Inserts the widget to the `0` index of the layout if the `layout` is a `PanelLayout` and the widget is not yet part of the layout.
34+
* Otherwise, it's NOOP
35+
* @param layout the layout to add the widget to. Must be a `PanelLayout`.
36+
* @param toAdd the widget to add to the layout
37+
*/
38+
export function unshiftWidgetIfNotPresent(
39+
layout: Layout | null,
40+
toAdd: Widget
41+
): void {
42+
if (layout instanceof PanelLayout) {
43+
const index = layout.widgets.indexOf(toAdd);
44+
if (index >= 0) {
45+
// Do not try to add the widget to the layout if it's already present.
46+
// This is the counterpart logic of the `removeWidgetIfPresent` function.
47+
return;
48+
}
49+
layout.insertWidget(0, toAdd);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import {
2+
Disposable,
3+
DisposableCollection,
4+
} from '@theia/core/lib/common/disposable';
5+
import type { PanelLayout, Widget } from '@theia/core/shared/@phosphor/widgets';
6+
import { expect } from 'chai';
7+
import type { unshiftWidgetIfNotPresent } from '../../browser/theia/dialogs/widgets';
8+
9+
describe('widgets', () => {
10+
let toDispose: DisposableCollection;
11+
12+
beforeEach(() => {
13+
const disableJSDOM =
14+
require('@theia/core/lib/browser/test/jsdom').enableJSDOM();
15+
toDispose = new DisposableCollection(
16+
Disposable.create(() => disableJSDOM())
17+
);
18+
});
19+
20+
afterEach(() => toDispose.dispose());
21+
22+
describe('removeWidgetIfPresent', () => {
23+
let testMe: typeof unshiftWidgetIfNotPresent;
24+
25+
beforeEach(
26+
() =>
27+
(testMe =
28+
require('../../browser/theia/dialogs/widgets').removeWidgetIfPresent)
29+
);
30+
31+
it('should remove the widget if present', () => {
32+
const layout = newPanelLayout();
33+
const widget = newWidget();
34+
layout.addWidget(widget);
35+
const toRemoveWidget = newWidget();
36+
layout.addWidget(toRemoveWidget);
37+
38+
expect(layout.widgets).to.be.deep.equal([widget, toRemoveWidget]);
39+
40+
testMe(layout, toRemoveWidget);
41+
42+
expect(layout.widgets).to.be.deep.equal([widget]);
43+
});
44+
45+
it('should be noop if the widget is not part of the layout', () => {
46+
const layout = newPanelLayout();
47+
const widget = newWidget();
48+
layout.addWidget(widget);
49+
50+
expect(layout.widgets).to.be.deep.equal([widget]);
51+
52+
testMe(layout, newWidget());
53+
54+
expect(layout.widgets).to.be.deep.equal([widget]);
55+
});
56+
});
57+
58+
describe('unshiftWidgetIfNotPresent', () => {
59+
let testMe: typeof unshiftWidgetIfNotPresent;
60+
61+
beforeEach(
62+
() =>
63+
(testMe =
64+
require('../../browser/theia/dialogs/widgets').unshiftWidgetIfNotPresent)
65+
);
66+
67+
it('should unshift the widget if not present', () => {
68+
const layout = newPanelLayout();
69+
const widget = newWidget();
70+
layout.addWidget(widget);
71+
72+
expect(layout.widgets).to.be.deep.equal([widget]);
73+
74+
const toAdd = newWidget();
75+
testMe(layout, toAdd);
76+
77+
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
78+
});
79+
80+
it('should be NOOP if widget is already part of the layout (at 0 index)', () => {
81+
const layout = newPanelLayout();
82+
const toAdd = newWidget();
83+
layout.addWidget(toAdd);
84+
const widget = newWidget();
85+
layout.addWidget(widget);
86+
87+
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
88+
89+
testMe(layout, toAdd);
90+
91+
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
92+
});
93+
94+
it('should be NOOP if widget is already part of the layout (at >0 index)', () => {
95+
const layout = newPanelLayout();
96+
const widget = newWidget();
97+
layout.addWidget(widget);
98+
const toAdd = newWidget();
99+
layout.addWidget(toAdd);
100+
101+
expect(layout.widgets).to.be.deep.equal([widget, toAdd]);
102+
103+
testMe(layout, toAdd);
104+
105+
expect(layout.widgets).to.be.deep.equal([widget, toAdd]);
106+
});
107+
});
108+
109+
function newWidget(): Widget {
110+
const { Widget } = require('@theia/core/shared/@phosphor/widgets');
111+
return new Widget();
112+
}
113+
114+
function newPanelLayout(): PanelLayout {
115+
const { PanelLayout } = require('@theia/core/shared/@phosphor/widgets');
116+
return new PanelLayout();
117+
}
118+
});

0 commit comments

Comments
 (0)