diff --git a/package.json b/package.json index cf33e48ae..3148b38c4 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "gulp": "^4.0.2", "gulp-concat": "^2.6.1", "gulp-dart-sass": "^1.0.2", + "jest": "^29.5.0", "jest-environment-jsdom": "^29.4.3", "jest-sonar-reporter": "^2.0.0", "jsdom": "^21.1.0", @@ -58,14 +59,12 @@ "ts-jest": "^29.0.5", "ts-loader": "^9.4.2", "tslib": "^2.5.0", + "ts-node": "^10.9.1", "typedoc": "^0.24.7", "typescript": "^4.9.5", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1" }, - "dependencies": { - "jest": "^29.5.0", - "ts-node": "^10.9.1" - } -} + "dependencies": {} +} \ No newline at end of file diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 173bd788e..2142045d5 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -46,6 +46,7 @@ class PanelContentPartTest implements IContentRenderer { dispose(): void { this.isDisposed = true; this._onDidDispose.fire(); + this._onDidDispose.dispose(); } } @@ -80,6 +81,7 @@ class PanelTabPartTest implements ITabRenderer { dispose(): void { this.isDisposed = true; this._onDidDispose.fire(); + this._onDidDispose.dispose(); } } @@ -98,6 +100,68 @@ describe('dockviewComponent', () => { }); }); + test('event leakage', () => { + Emitter.setLeakageMonitorEnabled(true); + + dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + }); + + dockview.layout(500, 1000); + + dockview.addPanel({ + id: 'panel1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + }); + + dockview.removePanel(panel2); + + const panel3 = dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'right', + referencePanel: 'panel1', + }, + }); + + const panel4 = dockview.addPanel({ + id: 'panel4', + component: 'default', + position: { + direction: 'above', + }, + }); + + dockview.moveGroupOrPanel( + panel4.group, + panel3.group.id, + panel3.id, + 'center' + ); + + dockview.dispose(); + + if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + for (const entry of Array.from( + Emitter.MEMORY_LEAK_WATCHER.events + )) { + console.log('disposal', entry[1]); + } + throw new Error('not all listeners disposed'); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + test('duplicate panel', () => { dockview.layout(500, 1000); @@ -112,6 +176,8 @@ describe('dockviewComponent', () => { component: 'default', }); }).toThrowError('panel with id panel1 already exists'); + + dockview.dispose(); }); test('set active panel', () => { @@ -1285,21 +1351,21 @@ describe('dockviewComponent', () => { tabComponent: 'default', }); - const panel2 = dockview.addPanel({ - id: 'panel2', - component: 'default', - tabComponent: 'default', - }); + // const panel2 = dockview.addPanel({ + // id: 'panel2', + // component: 'default', + // tabComponent: 'default', + // }); - expect(panel1.group).toEqual(panel2.group); + // expect(panel1.group).toEqual(panel2.group); const panel1Spy = jest.spyOn(panel1, 'dispose'); - const panel2Spy = jest.spyOn(panel2, 'dispose'); + // const panel2Spy = jest.spyOn(panel2, 'dispose'); dockview.dispose(); expect(panel1Spy).toBeCalledTimes(1); - expect(panel2Spy).toBeCalledTimes(1); + // expect(panel2Spy).toBeCalledTimes(1); }); test('panel is disposed of when from JSON is called', () => { diff --git a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts index 60d94d513..23c9b1df8 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitview.spec.ts @@ -585,8 +585,15 @@ describe('splitview', () => { expect(container.childNodes.length).toBeGreaterThan(0); - splitview.dispose(); + let anyEvents = false; + const listener = splitview.onDidRemoveView((e) => { + anyEvents = true; // disposing of the splitview shouldn't fire onDidRemoveView events + }); + splitview.dispose(); + listener.dispose(); + + expect(anyEvents).toBeFalsy(); expect(container.childNodes.length).toBe(0); }); }); diff --git a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts index e3c16e751..e43c94239 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts @@ -1,4 +1,5 @@ import { PanelDimensionChangeEvent } from '../../api/panelApi'; +import { Emitter } from '../../events'; import { CompositeDisposable } from '../../lifecycle'; import { Orientation } from '../../splitview/splitview'; import { SplitviewComponent } from '../../splitview/splitviewComponent'; @@ -25,6 +26,45 @@ describe('componentSplitview', () => { container.className = 'container'; }); + test('event leakage', () => { + Emitter.setLeakageMonitorEnabled(true); + + const splitview = new SplitviewComponent({ + parentElement: container, + orientation: Orientation.VERTICAL, + components: { + testPanel: TestPanel, + }, + }); + splitview.layout(600, 400); + + const panel1 = splitview.addPanel({ + id: 'panel1', + component: 'testPanel', + }); + const panel2 = splitview.addPanel({ + id: 'panel2', + component: 'testPanel', + }); + + splitview.movePanel(0, 1); + + splitview.removePanel(panel1); + + splitview.dispose(); + + if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + for (const entry of Array.from( + Emitter.MEMORY_LEAK_WATCHER.events + )) { + console.log(entry[1]); + } + throw new Error('not all listeners disposed'); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + test('remove panel', () => { const splitview = new SplitviewComponent({ parentElement: container, diff --git a/packages/dockview-core/src/api/panelApi.ts b/packages/dockview-core/src/api/panelApi.ts index 37648cd84..795ac1589 100644 --- a/packages/dockview-core/src/api/panelApi.ts +++ b/packages/dockview-core/src/api/panelApi.ts @@ -126,15 +126,6 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { super(); this.addDisposables( - this.panelUpdatesDisposable, - this._onDidDimensionChange, - this._onDidChangeFocus, - this._onDidVisibilityChange, - this._onDidActiveChange, - this._onFocusEvent, - this._onActiveChange, - this._onVisibilityChange, - this._onUpdateParameters, this.onDidFocusChange((event) => { this._isFocused = event.isFocused; }), @@ -147,7 +138,16 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this.onDidDimensionsChange((event) => { this._width = event.width; this._height = event.height; - }) + }), + this.panelUpdatesDisposable, + this._onDidDimensionChange, + this._onDidChangeFocus, + this._onDidVisibilityChange, + this._onDidActiveChange, + this._onFocusEvent, + this._onActiveChange, + this._onVisibilityChange, + this._onUpdateParameters ); } diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 2120b8fc5..df3f49177 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -16,6 +16,9 @@ export abstract class DragHandler extends CompositeDisposable { constructor(protected readonly el: HTMLElement) { super(); + + this.addDisposables(this._onDragStart); + this.configure(); } diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index cac6d30ad..12fcb0ea0 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -177,6 +177,7 @@ export class Droptarget extends CompositeDisposable { public dispose(): void { this.removeDropTarget(); + super.dispose(); } private toggleClasses( diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index e7f99e062..bdd183182 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -53,8 +53,4 @@ export class GroupDragHandler extends DragHandler { }, }; } - - public dispose(): void { - // - } } diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 75859e4d2..9934c5ec4 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -77,11 +77,12 @@ export class ContentContainer const _onDidFocus = this.panel.view.content.onDidFocus; const _onDidBlur = this.panel.view.content.onDidBlur; - const { onDidFocus, onDidBlur } = trackFocus(this._element); + const focusTracker = trackFocus(this._element); disposable.addDisposables( - onDidFocus(() => this._onDidFocus.fire()), - onDidBlur(() => this._onDidBlur.fire()) + focusTracker, + focusTracker.onDidFocus(() => this._onDidFocus.fire()), + focusTracker.onDidBlur(() => this._onDidBlur.fire()) ); if (_onDidFocus) { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 550752e53..15c81206b 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -12,7 +12,7 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; -export interface ITab { +export interface ITab extends IDisposable { readonly panelId: string; readonly element: HTMLElement; setContent: (element: ITabRenderer) => void; @@ -43,8 +43,6 @@ export class Tab extends CompositeDisposable implements ITab { ) { super(); - this.addDisposables(this._onChanged, this._onDropped); - this._element = document.createElement('div'); this._element.className = 'tab'; this._element.tabIndex = 0; @@ -53,6 +51,8 @@ export class Tab extends CompositeDisposable implements ITab { toggleClass(this.element, 'inactive-tab', true); this.addDisposables( + this._onChanged, + this._onDropped, new (class Handler extends DragHandler { private readonly panelTransfer = LocalSelectionTransfer.getInstance(); @@ -71,10 +71,6 @@ export class Tab extends CompositeDisposable implements ITab { }, }; } - - public dispose(): void { - // - } })(this._element) ); @@ -127,7 +123,8 @@ export class Tab extends CompositeDisposable implements ITab { this.addDisposables( this.droptarget.onDrop((event) => { this._onDropped.fire(event); - }) + }), + this.droptarget ); } @@ -146,6 +143,5 @@ export class Tab extends CompositeDisposable implements ITab { public dispose(): void { super.dispose(); - this.droptarget.dispose(); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index debdf42e0..68d8cfe1b 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -216,6 +216,7 @@ export class TabsContainer const { value, disposable } = tabToRemove; disposable.dispose(); + value.dispose(); value.element.remove(); } @@ -275,9 +276,11 @@ export class TabsContainer public dispose(): void { super.dispose(); - this.tabs.forEach((tab) => { - tab.disposable.dispose(); - }); + for (const { value, disposable } of this.tabs) { + disposable.dispose(); + value.dispose(); + } + this.tabs = []; } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 6d58c0b67..c180ad022 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -250,7 +250,6 @@ export class DockviewComponent }); this.addDisposables( - dropTarget, dropTarget.onDrop((event) => { const data = getPanelData(); @@ -269,7 +268,8 @@ export class DockviewComponent getData: getPanelData, }); } - }) + }), + dropTarget ); this._api = new DockviewApi(this); @@ -705,43 +705,49 @@ export class DockviewComponent } moveGroupOrPanel( - referenceGroup: DockviewGroupPanel, - groupId: string, - itemId: string | undefined, - target: Position, - index?: number + destinationGroup: DockviewGroupPanel, + sourceGroupId: string, + sourceItemId: string | undefined, + destinationTarget: Position, + destinationIndex?: number ): void { - const sourceGroup = groupId - ? this._groups.get(groupId)?.value + const sourceGroup = sourceGroupId + ? this._groups.get(sourceGroupId)?.value : undefined; - if (itemId === undefined) { + if (sourceItemId === undefined) { if (sourceGroup) { - this.moveGroup(sourceGroup, referenceGroup, target); + this.moveGroup( + sourceGroup, + destinationGroup, + destinationTarget + ); } return; } - if (!target || target === 'center') { + if (!destinationTarget || destinationTarget === 'center') { const groupItem: IDockviewPanel | undefined = - sourceGroup?.model.removePanel(itemId) || - this.panels.find((panel) => panel.id === itemId); + sourceGroup?.model.removePanel(sourceItemId) || + this.panels.find((panel) => panel.id === sourceItemId); if (!groupItem) { - throw new Error(`No panel with id ${itemId}`); + throw new Error(`No panel with id ${sourceItemId}`); } if (sourceGroup?.model.size === 0) { this.doRemoveGroup(sourceGroup); } - referenceGroup.model.openPanel(groupItem, { index }); + destinationGroup.model.openPanel(groupItem, { + index: destinationIndex, + }); } else { - const referenceLocation = getGridLocation(referenceGroup.element); + const referenceLocation = getGridLocation(destinationGroup.element); const targetLocation = getRelativeLocation( this.gridview.orientation, referenceLocation, - target + destinationTarget ); if (sourceGroup && sourceGroup.size < 2) { @@ -765,28 +771,28 @@ export class DockviewComponent // after deleting the group we need to re-evaulate the ref location const updatedReferenceLocation = getGridLocation( - referenceGroup.element + destinationGroup.element ); const location = getRelativeLocation( this.gridview.orientation, updatedReferenceLocation, - target + destinationTarget ); this.doAddGroup(targetGroup, location); } } else { const groupItem: IDockviewPanel | undefined = - sourceGroup?.model.removePanel(itemId) || - this.panels.find((panel) => panel.id === itemId); + sourceGroup?.model.removePanel(sourceItemId) || + this.panels.find((panel) => panel.id === sourceItemId); if (!groupItem) { - throw new Error(`No panel with id ${itemId}`); + throw new Error(`No panel with id ${sourceItemId}`); } const dropLocation = getRelativeLocation( this.gridview.orientation, referenceLocation, - target + destinationTarget ); const group = this.createGroupAtLocation(dropLocation); @@ -952,11 +958,11 @@ export class DockviewComponent } public dispose(): void { - super.dispose(); - this._onDidActivePanelChange.dispose(); this._onDidAddPanel.dispose(); this._onDidRemovePanel.dispose(); this._onDidLayoutFromJSON.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 5e9773a73..11bed8de9 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -282,12 +282,6 @@ export class DockviewGroupPanelModel this.locked = !!options.locked; this.addDisposables( - this._onMove, - this._onDidChange, - this._onDidDrop, - this._onDidAddPanel, - this._onDidRemovePanel, - this._onDidActivePanelChange, this.tabsContainer.onDrop((event) => { this.handleDropEvent(event.event, 'center', event.index); }), @@ -299,7 +293,13 @@ export class DockviewGroupPanelModel }), this.dropTarget.onDrop((event) => { this.handleDropEvent(event.nativeEvent, event.position); - }) + }), + this._onMove, + this._onDidChange, + this._onDidDrop, + this._onDidAddPanel, + this._onDidRemovePanel, + this._onDidActivePanelChange ); } diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index eb1a37c05..4a36f4bde 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -111,6 +111,8 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { constructor(element: HTMLElement | Window) { super(); + this.addDisposables(this._onDidFocus, this._onDidBlur); + let hasFocus = isAncestor(document.activeElement, element); let loosingFocus = false; @@ -169,11 +171,4 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { refreshState(): void { this._refreshStateHandler(); } - - public dispose(): void { - super.dispose(); - - this._onDidBlur.dispose(); - this._onDidFocus.dispose(); - } } diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 7f6b07ebf..13b8b3382 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -24,24 +24,76 @@ export namespace Event { }; } -// dumb event emitter with better typings than nodes event emitter -// https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts +class LeakageMonitor { + readonly events = new Map, Stacktrace>(); + + get size(): number { + return this.events.size; + } + + add(event: Event, stacktrace: Stacktrace): void { + this.events.set(event, stacktrace); + } + + delete(event: Event): void { + this.events.delete(event); + } + + clear(): void { + this.events.clear(); + } +} + +class Stacktrace { + static create(): Stacktrace { + return new Stacktrace(new Error().stack ?? ''); + } + + private constructor(readonly value: string) {} + + print(): void { + console.warn(this.value); + } +} + +class Listener { + constructor( + readonly callback: (t: T) => void, + readonly stacktrace: Stacktrace | undefined + ) {} +} + +// relatively simple event emitter taken from https://github.com/microsoft/vscode/blob/master/src/vs/base/common/event.ts export class Emitter implements IDisposable { private _event?: Event; private _last?: T; - private _listeners: Array<(e: T) => any> = []; + private _listeners: Listener[] = []; private _disposed = false; + static ENABLE_TRACKING = false; + static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor(); + + static setLeakageMonitorEnabled(isEnabled: boolean) { + if (isEnabled !== Emitter.ENABLE_TRACKING) { + Emitter.MEMORY_LEAK_WATCHER.clear(); + } + Emitter.ENABLE_TRACKING = isEnabled; + } + constructor(private readonly options?: EmitterOptions) {} get event(): Event { if (!this._event) { - this._event = (listener: (e: T) => void): IDisposable => { + this._event = (callback: (e: T) => void): IDisposable => { if (this.options?.replay && this._last !== undefined) { - listener(this._last); + callback(this._last); } + const listener = new Listener( + callback, + Emitter.ENABLE_TRACKING ? Stacktrace.create() : undefined + ); this._listeners.push(listener); return { @@ -49,10 +101,22 @@ export class Emitter implements IDisposable { const index = this._listeners.indexOf(listener); if (index > -1) { this._listeners.splice(index, 1); + } else if (Emitter.ENABLE_TRACKING) { + console.warn( + `Listener already disposed`, + Stacktrace.create().print() + ); } }, }; }; + + if (Emitter.ENABLE_TRACKING) { + Emitter.MEMORY_LEAK_WATCHER.add( + this._event, + Stacktrace.create() + ); + } } return this._event; } @@ -60,13 +124,31 @@ export class Emitter implements IDisposable { public fire(e: T): void { this._last = e; for (const listener of this._listeners) { - listener(e); + listener.callback(e); } } public dispose(): void { - this._listeners = []; - this._disposed = true; + if (!this._disposed) { + this._disposed = true; + + if (this._listeners.length > 0) { + if (Emitter.ENABLE_TRACKING) { + queueMicrotask(() => { + // don't check until stack of execution is completed to allow for out-of-order disposals within the same execution block + for (const listener of this._listeners) { + console.warn(listener.stacktrace?.print()); + } + }); + } + + this._listeners = []; + } + + if (Emitter.ENABLE_TRACKING && this._event) { + Emitter.MEMORY_LEAK_WATCHER.delete(this._event); + } + } } } diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 7f2c281df..93c6861eb 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -143,10 +143,7 @@ export abstract class BaseGrid this.addDisposables( this.gridview.onDidChange(() => { this._bufferOnDidLayoutChange.fire(); - }) - ); - - this.addDisposables( + }), Event.any( this.onDidAddGroup, this.onDidRemoveGroup, @@ -297,8 +294,6 @@ export abstract class BaseGrid } public dispose(): void { - super.dispose(); - this._onDidActiveGroupChange.dispose(); this._onDidAddGroup.dispose(); this._onDidRemoveGroup.dispose(); @@ -309,5 +304,7 @@ export abstract class BaseGrid } this.gridview.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/basePanelView.ts b/packages/dockview-core/src/gridview/basePanelView.ts index 375b37a47..f61473864 100644 --- a/packages/dockview-core/src/gridview/basePanelView.ts +++ b/packages/dockview-core/src/gridview/basePanelView.ts @@ -68,16 +68,17 @@ export abstract class BasePanelView this._element.style.width = '100%'; this._element.style.overflow = 'hidden'; - const { onDidFocus, onDidBlur } = trackFocus(this._element); + const focusTracker = trackFocus(this._element); this.addDisposables( this.api, - onDidFocus(() => { + focusTracker.onDidFocus(() => { this.api._onDidChangeFocus.fire({ isFocused: true }); }), - onDidBlur(() => { + focusTracker.onDidBlur(() => { this.api._onDidChangeFocus.fire({ isFocused: false }); - }) + }), + focusTracker ); } @@ -124,9 +125,9 @@ export abstract class BasePanelView } dispose(): void { - super.dispose(); - this.api.dispose(); this.part?.dispose(); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index c97e33243..6295b3f88 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -260,13 +260,13 @@ export class BranchNode extends CompositeDisposable implements IView { return this.splitview.getViewCachedVisibleSize(index); } - public removeChild(index: number, sizing?: Sizing): void { + public removeChild(index: number, sizing?: Sizing): Node { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } this.splitview.removeView(index, sizing); - this._removeChild(index); + return this._removeChild(index); } private _addChild(node: Node, index: number): void { @@ -296,9 +296,10 @@ export class BranchNode extends CompositeDisposable implements IView { } public dispose(): void { - super.dispose(); this._childrenDisposable.dispose(); - this.children.forEach((child) => child.dispose()); this.splitview.dispose(); + this.children.forEach((child) => child.dispose()); + + super.dispose(); } } diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 37242cc33..583b63c6f 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -462,7 +462,8 @@ export class Gridview implements IDisposable { if (oldRoot.children.length === 1) { // can remove one level of redundant branching if there is only a single child const childReference = oldRoot.children[0]; - oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root + const child = oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root + child.dispose(); oldRoot.dispose(); this._root.addChild( @@ -632,7 +633,8 @@ export class Gridview implements IDisposable { newSiblingSize = Sizing.Invisible(newSiblingCachedVisibleSize); } - grandParent.removeChild(parentIndex); + const child = grandParent.removeChild(parentIndex); + child.dispose(); const newParent = new BranchNode( parent.orientation, @@ -682,14 +684,18 @@ export class Gridview implements IDisposable { throw new Error('Invalid location'); } - parent.removeChild(index, sizing); + const view = node.view; + node.dispose(); // dispose of node + + const child = parent.removeChild(index, sizing); + child.dispose(); if (parent.children.length === 0) { - return node.view; + return view; } if (parent.children.length > 1) { - return node.view; + return view; } const sibling = parent.children[0]; @@ -698,25 +704,28 @@ export class Gridview implements IDisposable { // parent is root if (sibling instanceof LeafNode) { - return node.view; + return view; } // we must promote sibling to be the new root - parent.removeChild(0, sizing); + const child = parent.removeChild(0, sizing); + child.dispose(); this.root = sibling; - return node.view; + return view; } const [grandParent, ..._] = [...pathToParent].reverse(); const [parentIndex, ...__] = [...rest].reverse(); const isSiblingVisible = parent.isChildVisible(0); - parent.removeChild(0, sizing); + const childNode = parent.removeChild(0, sizing); + childNode.dispose(); const sizes = grandParent.children.map((_size, i) => grandParent.getChildSize(i) ); - grandParent.removeChild(parentIndex, sizing); + const parentNode = grandParent.removeChild(parentIndex, sizing); + parentNode.dispose(); if (sibling instanceof BranchNode) { sizes.splice( @@ -745,7 +754,7 @@ export class Gridview implements IDisposable { grandParent.resizeChild(i, sizes[i]); } - return node.view; + return view; } public layout(width: number, height: number): void { diff --git a/packages/dockview-core/src/gridview/gridviewPanel.ts b/packages/dockview-core/src/gridview/gridviewPanel.ts index 530a3cc70..9562bb92b 100644 --- a/packages/dockview-core/src/gridview/gridviewPanel.ts +++ b/packages/dockview-core/src/gridview/gridviewPanel.ts @@ -154,7 +154,6 @@ export abstract class GridviewPanel this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.addDisposables( - this._onDidChange, this.api.onVisibilityChange((event) => { const { isVisible } = event; const { accessor } = this._params as GridviewInitParameters; @@ -195,7 +194,8 @@ export abstract class GridviewPanel height: event.height, width: event.width, }); - }) + }), + this._onDidChange ); } diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index e756bc803..8262b28d1 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -16,7 +16,7 @@ export namespace Disposable { } export class CompositeDisposable { - private readonly disposables: IDisposable[]; + private readonly _disposables: IDisposable[]; private _isDisposed = false; protected get isDisposed(): boolean { @@ -28,15 +28,15 @@ export class CompositeDisposable { } constructor(...args: IDisposable[]) { - this.disposables = args; + this._disposables = args; } public addDisposables(...args: IDisposable[]): void { - args.forEach((arg) => this.disposables.push(arg)); + args.forEach((arg) => this._disposables.push(arg)); } public dispose(): void { - this.disposables.forEach((arg) => arg.dispose()); + this._disposables.forEach((arg) => arg.dispose()); this._isDisposed = true; } diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 447529cfe..48295a8c7 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -13,6 +13,7 @@ import { Event, Emitter } from '../events'; import { pushToStart, pushToEnd, firstIndex } from '../array'; import { range, clamp } from '../math'; import { ViewItem } from './viewItem'; +import { IDisposable } from '../lifecycle'; export enum Orientation { HORIZONTAL = 'HORIZONTAL', @@ -42,7 +43,7 @@ export enum LayoutPriority { Normal = 'normal', } -export interface IBaseView { +export interface IBaseView extends IDisposable { minimumSize: number; maximumSize: number; snap?: boolean; @@ -97,7 +98,7 @@ export class Splitview { private element: HTMLElement; private viewContainer: HTMLElement; private sashContainer: HTMLElement; - private views: ViewItem[] = []; + private viewItems: ViewItem[] = []; private sashes: ISashItem[] = []; private _orientation: Orientation; private _size = 0; @@ -132,7 +133,7 @@ export class Splitview { } public get length(): number { - return this.views.length; + return this.viewItems.length; } public get proportions(): number[] | undefined { @@ -159,13 +160,13 @@ export class Splitview { } get minimumSize(): number { - return this.views.reduce((r, item) => r + item.minimumSize, 0); + return this.viewItems.reduce((r, item) => r + item.minimumSize, 0); } get maximumSize(): number { return this.length === 0 ? Number.POSITIVE_INFINITY - : this.views.reduce((r, item) => r + item.maximumSize, 0); + : this.viewItems.reduce((r, item) => r + item.maximumSize, 0); } get startSnappingEnabled(): boolean { @@ -240,7 +241,7 @@ export class Splitview { }); // Initialize content size and proportions for first layout - this.contentSize = this.views.reduce((r, i) => r + i.size, 0); + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.saveProportions(); } } @@ -261,22 +262,22 @@ export class Splitview { } isViewVisible(index: number): boolean { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; return viewItem.visible; } setViewVisible(index: number, visible: boolean): void { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } toggleClass(this.container, 'visible', visible); - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; toggleClass(this.container, 'visible', visible); @@ -288,30 +289,30 @@ export class Splitview { } getViewSize(index: number): number { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return -1; } - return this.views[index].size; + return this.viewItems[index].size; } resizeView(index: number, size: number): void { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return; } - const indexes = range(this.views.length).filter((i) => i !== index); + const indexes = range(this.viewItems.length).filter((i) => i !== index); const lowPriorityIndexes = [ ...indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ), index, ]; const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); - const item = this.views[index]; + const item = this.viewItems[index]; size = Math.round(size); size = clamp( size, @@ -324,13 +325,13 @@ export class Splitview { } public getViews(): T[] { - return this.views.map((x) => x.view as T); + return this.viewItems.map((x) => x.view as T); } private onDidChange(item: ViewItem, size: number | undefined): void { - const index = this.views.indexOf(item); + const index = this.viewItems.indexOf(item); - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { return; } @@ -345,7 +346,7 @@ export class Splitview { public addView( view: IView, size: number | Sizing = { type: 'distribute' }, - index: number = this.views.length, + index: number = this.viewItems.length, skipLayout?: boolean ): void { const container = document.createElement('div'); @@ -369,14 +370,14 @@ export class Splitview { this.onDidChange(viewItem, newSize.size) ); - const dispose = () => { - disposable?.dispose(); - this.viewContainer.removeChild(container); - }; + const viewItem = new ViewItem(container, view, viewSize, { + dispose: () => { + disposable.dispose(); + this.viewContainer.removeChild(container); + }, + }); - const viewItem = new ViewItem(container, view, viewSize, { dispose }); - - if (index === this.views.length) { + if (index === this.viewItems.length) { this.viewContainer.appendChild(container); } else { this.viewContainer.insertBefore( @@ -385,15 +386,15 @@ export class Splitview { ); } - this.views.splice(index, 0, viewItem); + this.viewItems.splice(index, 0, viewItem); - if (this.views.length > 1) { + if (this.viewItems.length > 1) { //add sash const sash = document.createElement('div'); sash.className = 'sash'; const onStart = (event: MouseEvent) => { - for (const item of this.views) { + for (const item of this.viewItems) { item.enabled = false; } @@ -417,19 +418,20 @@ export class Splitview { ); // - const sizes = this.views.map((x) => x.size); + const sizes = this.viewItems.map((x) => x.size); // let snapBefore: ISashDragSnapState | undefined; let snapAfter: ISashDragSnapState | undefined; const upIndexes = range(sashIndex, -1); - const downIndexes = range(sashIndex + 1, this.views.length); + const downIndexes = range(sashIndex + 1, this.viewItems.length); const minDeltaUp = upIndexes.reduce( - (r, i) => r + (this.views[i].minimumSize - sizes[i]), + (r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0 ); const maxDeltaUp = upIndexes.reduce( - (r, i) => r + (this.views[i].viewMaximumSize - sizes[i]), + (r, i) => + r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0 ); const maxDeltaDown = @@ -437,7 +439,8 @@ export class Splitview { ? Number.POSITIVE_INFINITY : downIndexes.reduce( (r, i) => - r + (sizes[i] - this.views[i].minimumSize), + r + + (sizes[i] - this.viewItems[i].minimumSize), 0 ); const minDeltaDown = @@ -446,7 +449,8 @@ export class Splitview { : downIndexes.reduce( (r, i) => r + - (sizes[i] - this.views[i].viewMaximumSize), + (sizes[i] - + this.viewItems[i].viewMaximumSize), 0 ); const minDelta = Math.max(minDeltaUp, minDeltaDown); @@ -454,7 +458,7 @@ export class Splitview { const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); if (typeof snapBeforeIndex === 'number') { - const snappedViewItem = this.views[snapBeforeIndex]; + const snappedViewItem = this.viewItems[snapBeforeIndex]; const halfSize = Math.floor( snappedViewItem.viewMinimumSize / 2 ); @@ -469,7 +473,7 @@ export class Splitview { } if (typeof snapAfterIndex === 'number') { - const snappedViewItem = this.views[snapAfterIndex]; + const snappedViewItem = this.viewItems[snapAfterIndex]; const halfSize = Math.floor( snappedViewItem.viewMinimumSize / 2 ); @@ -507,7 +511,7 @@ export class Splitview { }; const end = () => { - for (const item of this.views) { + for (const item of this.viewItems) { item.enabled = true; } @@ -562,7 +566,7 @@ export class Splitview { const flexibleViewItems: ViewItem[] = []; let flexibleSize = 0; - for (const item of this.views) { + for (const item of this.viewItems) { if (item.maximumSize - item.minimumSize > 0) { flexibleViewItems.push(item); flexibleSize += item.size; @@ -575,12 +579,12 @@ export class Splitview { item.size = clamp(size, item.minimumSize, item.maximumSize); } - const indexes = range(this.views.length); + const indexes = range(this.viewItems.length); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); this.relayout(lowPriorityIndexes, highPriorityIndexes); @@ -592,11 +596,11 @@ export class Splitview { skipLayout = false ): IView { // Remove view - const viewItem = this.views.splice(index, 1)[0]; + const viewItem = this.viewItems.splice(index, 1)[0]; viewItem.dispose(); // Remove sash - if (this.views.length >= 1) { + if (this.viewItems.length >= 1) { const sashIndex = Math.max(index - 1, 0); const sashItem = this.sashes.splice(sashIndex, 1)[0]; sashItem.disposable(); @@ -616,11 +620,11 @@ export class Splitview { } getViewCachedVisibleSize(index: number): number | undefined { - if (index < 0 || index >= this.views.length) { + if (index < 0 || index >= this.viewItems.length) { throw new Error('Index out of bounds'); } - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; return viewItem.cachedVisibleSize; } @@ -640,24 +644,24 @@ export class Splitview { this.orthogonalSize = orthogonalSize; if (!this.proportions) { - const indexes = range(this.views.length); + const indexes = range(this.viewItems.length); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); this.resize( - this.views.length - 1, + this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes ); } else { - for (let i = 0; i < this.views.length; i++) { - const item = this.views[i]; + for (let i = 0; i < this.viewItems.length; i++) { + const item = this.viewItems[i]; item.size = clamp( Math.round(this.proportions[i] * size), @@ -675,10 +679,10 @@ export class Splitview { lowPriorityIndexes?: number[], highPriorityIndexes?: number[] ): void { - const contentSize = this.views.reduce((r, i) => r + i.size, 0); + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); this.resize( - this.views.length - 1, + this.viewItems.length - 1, this._size - contentSize, undefined, lowPriorityIndexes, @@ -690,15 +694,15 @@ export class Splitview { } private distributeEmptySpace(lowPriorityIndex?: number): void { - const contentSize = this.views.reduce((r, i) => r + i.size, 0); + const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); let emptyDelta = this.size - contentSize; - const indexes = range(this.views.length - 1, -1); + const indexes = range(this.viewItems.length - 1, -1); const lowPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.Low + (i) => this.viewItems[i].priority === LayoutPriority.Low ); const highPriorityIndexes = indexes.filter( - (i) => this.views[i].priority === LayoutPriority.High + (i) => this.viewItems[i].priority === LayoutPriority.High ); for (const index of highPriorityIndexes) { @@ -714,7 +718,7 @@ export class Splitview { } for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) { - const item = this.views[indexes[i]]; + const item = this.viewItems[indexes[i]]; const size = clamp( item.size + emptyDelta, item.minimumSize, @@ -729,21 +733,21 @@ export class Splitview { private saveProportions(): void { if (this.proportionalLayout && this.contentSize > 0) { - this._proportions = this.views.map( + this._proportions = this.viewItems.map( (i) => i.size / this.contentSize ); } } private layoutViews(): void { - this.contentSize = this.views.reduce((r, i) => r + i.size, 0); + this.contentSize = this.viewItems.reduce((r, i) => r + i.size, 0); let sum = 0; const x: number[] = []; this.updateSashEnablement(); - for (let i = 0; i < this.views.length - 1; i++) { - sum += this.views[i].size; + for (let i = 0; i < this.viewItems.length - 1; i++) { + sum += this.viewItems[i].size; x.push(sum); const offset = Math.min(Math.max(0, sum - 2), this.size - 4); @@ -757,7 +761,7 @@ export class Splitview { this.sashes[i].container.style.top = `${offset}px`; } } - this.views.forEach((view, i) => { + this.viewItems.forEach((view, i) => { if (this._orientation === Orientation.HORIZONTAL) { view.container.style.width = `${view.size}px`; view.container.style.left = i == 0 ? '0px' : `${x[i - 1]}px`; @@ -778,7 +782,7 @@ export class Splitview { private findFirstSnapIndex(indexes: number[]): number | undefined { // visible views first for (const index of indexes) { - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; if (!viewItem.visible) { continue; @@ -791,7 +795,7 @@ export class Splitview { // then, hidden views for (const index of indexes) { - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; if ( viewItem.visible && @@ -810,16 +814,16 @@ export class Splitview { private updateSashEnablement(): void { let previous = false; - const collapsesDown = this.views.map( + const collapsesDown = this.viewItems.map( (i) => (previous = i.size - i.minimumSize > 0 || previous) ); previous = false; - const expandsDown = this.views.map( + const expandsDown = this.viewItems.map( (i) => (previous = i.maximumSize - i.size > 0 || previous) ); - const reverseViews = [...this.views].reverse(); + const reverseViews = [...this.viewItems].reverse(); previous = false; const collapsesUp = reverseViews .map((i) => (previous = i.size - i.minimumSize > 0 || previous)) @@ -833,7 +837,7 @@ export class Splitview { let position = 0; for (let index = 0; index < this.sashes.length; index++) { const sash = this.sashes[index]; - const viewItem = this.views[index]; + const viewItem = this.viewItems[index]; position += viewItem.size; const min = !(collapsesDown[index] && expandsUp[index + 1]); @@ -841,16 +845,16 @@ export class Splitview { if (min && max) { const upIndexes = range(index, -1); - const downIndexes = range(index + 1, this.views.length); + const downIndexes = range(index + 1, this.viewItems.length); const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); const snapAfterIndex = this.findFirstSnapIndex(downIndexes); const snappedBefore = typeof snapBeforeIndex === 'number' && - !this.views[snapBeforeIndex].visible; + !this.viewItems[snapBeforeIndex].visible; const snappedAfter = typeof snapAfterIndex === 'number' && - !this.views[snapAfterIndex].visible; + !this.viewItems[snapAfterIndex].visible; if ( snappedBefore && @@ -887,7 +891,7 @@ export class Splitview { private resize = ( index: number, delta: number, - sizes: number[] = this.views.map((x) => x.size), + sizes: number[] = this.viewItems.map((x) => x.size), lowPriorityIndexes?: number[], highPriorityIndexes?: number[], overloadMinDelta: number = Number.NEGATIVE_INFINITY, @@ -895,12 +899,12 @@ export class Splitview { snapBefore?: ISashDragSnapState, snapAfter?: ISashDragSnapState ): number => { - if (index < 0 || index > this.views.length) { + if (index < 0 || index > this.viewItems.length) { return 0; } const upIndexes = range(index, -1); - const downIndexes = range(index + 1, this.views.length); + const downIndexes = range(index + 1, this.viewItems.length); // if (highPriorityIndexes) { for (const i of highPriorityIndexes) { @@ -916,18 +920,18 @@ export class Splitview { } } // - const upItems = upIndexes.map((i) => this.views[i]); + const upItems = upIndexes.map((i) => this.viewItems[i]); const upSizes = upIndexes.map((i) => sizes[i]); // - const downItems = downIndexes.map((i) => this.views[i]); + const downItems = downIndexes.map((i) => this.viewItems[i]); const downSizes = downIndexes.map((i) => sizes[i]); // const minDeltaUp = upIndexes.reduce( - (_, i) => _ + this.views[i].minimumSize - sizes[i], + (_, i) => _ + this.viewItems[i].minimumSize - sizes[i], 0 ); const maxDeltaUp = upIndexes.reduce( - (_, i) => _ + this.views[i].maximumSize - sizes[i], + (_, i) => _ + this.viewItems[i].maximumSize - sizes[i], 0 ); // @@ -935,7 +939,7 @@ export class Splitview { downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce( - (_, i) => _ + sizes[i] - this.views[i].minimumSize, + (_, i) => _ + sizes[i] - this.viewItems[i].minimumSize, 0 ); @@ -943,7 +947,7 @@ export class Splitview { downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce( - (_, i) => _ + sizes[i] - this.views[i].maximumSize, + (_, i) => _ + sizes[i] - this.viewItems[i].maximumSize, 0 ); // @@ -952,14 +956,14 @@ export class Splitview { // let snapped = false; if (snapBefore) { - const snapView = this.views[snapBefore.index]; + const snapView = this.viewItems[snapBefore.index]; const visible = delta >= snapBefore.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapBefore.size); } if (!snapped && snapAfter) { - const snapView = this.views[snapAfter.index]; + const snapView = this.viewItems[snapAfter.index]; const visible = delta < snapAfter.limitDelta; snapped = visible !== snapView.visible; snapView.setVisible(visible, snapAfter.size); @@ -1047,6 +1051,10 @@ export class Splitview { } } + for (const viewItem of this.viewItems) { + viewItem.dispose(); + } + this.element.remove(); } } diff --git a/packages/dockview-core/src/splitview/splitviewComponent.ts b/packages/dockview-core/src/splitview/splitviewComponent.ts index f4f0bee7c..37573c40e 100644 --- a/packages/dockview-core/src/splitview/splitviewComponent.ts +++ b/packages/dockview-core/src/splitview/splitviewComponent.ts @@ -1,7 +1,6 @@ import { CompositeDisposable, IDisposable, - IValueDisposable, MutableDisposable, } from '../lifecycle'; import { @@ -83,10 +82,10 @@ export class SplitviewComponent extends Resizable implements ISplitviewComponent { - private _disposable = new MutableDisposable(); + private _splitviewChangeDisposable = new MutableDisposable(); private _splitview!: Splitview; private _activePanel: SplitviewPanel | undefined; - private _panels = new Map>(); + private _panels = new Map(); private _options: SplitviewComponentOptions; private readonly _onDidLayoutfromJSON = new Emitter(); @@ -124,7 +123,7 @@ export class SplitviewComponent set splitview(value: Splitview) { this._splitview = value; - this._disposable.value = new CompositeDisposable( + this._splitviewChangeDisposable.value = new CompositeDisposable( this._splitview.onDidSashEnd(() => { this._onDidLayoutChange.fire(undefined); }), @@ -170,7 +169,6 @@ export class SplitviewComponent this.splitview = new Splitview(this.element, options); this.addDisposables( - this._disposable, this._onDidAddView, this._onDidLayoutfromJSON, this._onDidRemoveView, @@ -226,19 +224,19 @@ export class SplitviewComponent } removePanel(panel: SplitviewPanel, sizing?: Sizing): void { - const disposable = this._panels.get(panel.id); + const item = this._panels.get(panel.id); - if (!disposable) { + if (!item) { throw new Error(`unknown splitview panel ${panel.id}`); } - disposable.disposable.dispose(); - disposable.value.dispose(); + item.dispose(); this._panels.delete(panel.id); const index = this.panels.findIndex((_) => _ === panel); - this.splitview.removeView(index, sizing); + const removedView = this.splitview.removeView(index, sizing); + removedView.dispose(); const panels = this.panels; if (panels.length > 0) { @@ -250,7 +248,7 @@ export class SplitviewComponent return this.panels.find((view) => view.id === id); } - addPanel(options: AddSplitviewComponentOptions): ISplitviewPanel { + addPanel(options: AddSplitviewComponentOptions): SplitviewPanel { if (this._panels.has(options.id)) { throw new Error(`panel ${options.id} already exists`); } @@ -308,7 +306,7 @@ export class SplitviewComponent this.setActive(view, true); }); - this._panels.set(view.id, { disposable, value: view }); + this._panels.set(view.id, disposable); } toJSON(): SerializedSplitview { @@ -404,23 +402,34 @@ export class SplitviewComponent } clear(): void { - for (const [_, value] of this._panels.entries()) { - value.disposable.dispose(); - value.value.dispose(); + for (const disposable of this._panels.values()) { + disposable.dispose(); } + this._panels.clear(); - this.splitview.dispose(); + + while (this.splitview.length > 0) { + const view = this.splitview.removeView(0, Sizing.Distribute, true); + view.dispose(); + } } dispose(): void { - for (const [_, value] of this._panels.entries()) { - value.disposable.dispose(); - value.value.dispose(); + for (const disposable of this._panels.values()) { + disposable.dispose(); } + this._panels.clear(); + const views = this.splitview.getViews(); + + this._splitviewChangeDisposable.dispose(); this.splitview.dispose(); + for (const view of views) { + view.dispose(); + } + super.dispose(); } } diff --git a/packages/dockview-core/src/splitview/splitviewPanel.ts b/packages/dockview-core/src/splitview/splitviewPanel.ts index 4782e8c30..d0ac1c41c 100644 --- a/packages/dockview-core/src/splitview/splitviewPanel.ts +++ b/packages/dockview-core/src/splitview/splitviewPanel.ts @@ -7,6 +7,7 @@ import { SplitviewPanelApiImpl } from '../api/splitviewPanelApi'; import { LayoutPriority, Orientation } from './splitview'; import { FunctionOrValue } from '../types'; import { Emitter, Event } from '../events'; +import { CompositeDisposable } from '../lifecycle'; export interface ISplitviewPanel extends BasePanelViewExported {