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`](9f5e11025b/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`](9f5e11025b/packages/algorithm/src/array.ts (L1075-L1077))
behavior.

Closes: arduino/arduino-ide#2354

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2024-02-08 10:21:09 +01:00
parent 8c1ffe9c5f
commit 470db5606e
3 changed files with 182 additions and 11 deletions

View File

@ -1,8 +1,5 @@
import { import { codicon } from '@theia/core/lib/browser/widgets/widget';
codicon, import { Widget } from '@theia/core/shared/@phosphor/widgets';
PanelLayout,
Widget,
} from '@theia/core/lib/browser/widgets/widget';
import { import {
inject, inject,
injectable, injectable,
@ -10,6 +7,10 @@ import {
} from '@theia/core/shared/inversify'; } from '@theia/core/shared/inversify';
import { DebugWidget as TheiaDebugWidget } from '@theia/debug/lib/browser/view/debug-widget'; import { DebugWidget as TheiaDebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { DebugDisabledStatusMessageSource } from '../../contributions/debug'; import { DebugDisabledStatusMessageSource } from '../../contributions/debug';
import {
removeWidgetIfPresent,
unshiftWidgetIfNotPresent,
} from '../dialogs/widgets';
@injectable() @injectable()
export class DebugWidget extends TheiaDebugWidget { export class DebugWidget extends TheiaDebugWidget {
@ -38,12 +39,10 @@ export class DebugWidget extends TheiaDebugWidget {
this.messageNode.textContent = message ?? ''; this.messageNode.textContent = message ?? '';
const enabled = !message; const enabled = !message;
updateVisibility(enabled, this.toolbar, this.sessionWidget); updateVisibility(enabled, this.toolbar, this.sessionWidget);
if (this.layout instanceof PanelLayout) { if (enabled) {
if (enabled) { removeWidgetIfPresent(this.layout, this.statusMessageWidget);
this.layout.removeWidget(this.statusMessageWidget); } else {
} else { unshiftWidgetIfNotPresent(this.layout, this.statusMessageWidget);
this.layout.insertWidget(0, this.statusMessageWidget);
}
} }
this.title.iconClass = enabled ? codicon('debug-alt') : 'fa fa-ban'; // TODO: find a better icon? this.title.iconClass = enabled ? codicon('debug-alt') : 'fa fa-ban'; // TODO: find a better icon?
}); });

View File

@ -0,0 +1,51 @@
import {
Layout,
PanelLayout,
Widget,
} from '@theia/core/shared/@phosphor/widgets';
/**
*
* Removes the widget from the layout if the `layout` is a `PanelLayout` and the widget is present in the layout.
* Otherwise, it's NOOP
* @param layout the layout to remove the widget from. Must be a `PanelLayout`.
* @param toRemove the widget to remove from the layout
*/
export function removeWidgetIfPresent(
layout: Layout | null,
toRemove: Widget
): void {
if (layout instanceof PanelLayout) {
const index = layout.widgets.indexOf(toRemove);
if (index < 0) {
// Unlike the default `PanelLayout#removeWidget` behavior, (https://github.com/phosphorjs/phosphor/blob/9f5e11025b62d2c4a6fb59e2681ae1ed323dcde4/packages/widgets/src/panellayout.ts#L154-L156)
// do not try to remove widget if it's not present (the index is negative).
// 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)
// See https://github.com/arduino/arduino-ide/issues/2354 for more details.
return;
}
layout.removeWidget(toRemove);
}
}
/**
*
* 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.
* Otherwise, it's NOOP
* @param layout the layout to add the widget to. Must be a `PanelLayout`.
* @param toAdd the widget to add to the layout
*/
export function unshiftWidgetIfNotPresent(
layout: Layout | null,
toAdd: Widget
): void {
if (layout instanceof PanelLayout) {
const index = layout.widgets.indexOf(toAdd);
if (index >= 0) {
// Do not try to add the widget to the layout if it's already present.
// This is the counterpart logic of the `removeWidgetIfPresent` function.
return;
}
layout.insertWidget(0, toAdd);
}
}

View File

@ -0,0 +1,121 @@
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import type { PanelLayout, Widget } from '@theia/core/shared/@phosphor/widgets';
import { expect } from 'chai';
import type {
removeWidgetIfPresent,
unshiftWidgetIfNotPresent,
} from '../../browser/theia/dialogs/widgets';
describe('widgets', () => {
let toDispose: DisposableCollection;
beforeEach(() => {
const disableJSDOM =
require('@theia/core/lib/browser/test/jsdom').enableJSDOM();
toDispose = new DisposableCollection(
Disposable.create(() => disableJSDOM())
);
});
afterEach(() => toDispose.dispose());
describe('removeWidgetIfPresent', () => {
let testMe: typeof removeWidgetIfPresent;
beforeEach(
() =>
(testMe =
require('../../browser/theia/dialogs/widgets').removeWidgetIfPresent)
);
it('should remove the widget if present', () => {
const layout = newPanelLayout();
const widget = newWidget();
layout.addWidget(widget);
const toRemoveWidget = newWidget();
layout.addWidget(toRemoveWidget);
expect(layout.widgets).to.be.deep.equal([widget, toRemoveWidget]);
testMe(layout, toRemoveWidget);
expect(layout.widgets).to.be.deep.equal([widget]);
});
it('should be noop if the widget is not part of the layout', () => {
const layout = newPanelLayout();
const widget = newWidget();
layout.addWidget(widget);
expect(layout.widgets).to.be.deep.equal([widget]);
testMe(layout, newWidget());
expect(layout.widgets).to.be.deep.equal([widget]);
});
});
describe('unshiftWidgetIfNotPresent', () => {
let testMe: typeof unshiftWidgetIfNotPresent;
beforeEach(
() =>
(testMe =
require('../../browser/theia/dialogs/widgets').unshiftWidgetIfNotPresent)
);
it('should unshift the widget if not present', () => {
const layout = newPanelLayout();
const widget = newWidget();
layout.addWidget(widget);
expect(layout.widgets).to.be.deep.equal([widget]);
const toAdd = newWidget();
testMe(layout, toAdd);
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
});
it('should be NOOP if widget is already part of the layout (at 0 index)', () => {
const layout = newPanelLayout();
const toAdd = newWidget();
layout.addWidget(toAdd);
const widget = newWidget();
layout.addWidget(widget);
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
testMe(layout, toAdd);
expect(layout.widgets).to.be.deep.equal([toAdd, widget]);
});
it('should be NOOP if widget is already part of the layout (at >0 index)', () => {
const layout = newPanelLayout();
const widget = newWidget();
layout.addWidget(widget);
const toAdd = newWidget();
layout.addWidget(toAdd);
expect(layout.widgets).to.be.deep.equal([widget, toAdd]);
testMe(layout, toAdd);
expect(layout.widgets).to.be.deep.equal([widget, toAdd]);
});
});
function newWidget(): Widget {
const { Widget } = require('@theia/core/shared/@phosphor/widgets');
return new Widget();
}
function newPanelLayout(): PanelLayout {
const { PanelLayout } = require('@theia/core/shared/@phosphor/widgets');
return new PanelLayout();
}
});