diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 512d2d1225f..da31c69210e 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -42,12 +42,9 @@ export function toDisposable(fn: () => void): IDisposable { return { dispose() { fn(); } }; } -export abstract class Disposable implements IDisposable { - static None = Object.freeze({ dispose() { } }); - - protected _toDispose: IDisposable[] = []; - protected get toDispose(): IDisposable[] { return this._toDispose; } +export class DisposableStore implements IDisposable { + private _toDispose: IDisposable[] = []; private _lifecycle_disposable_isDisposed = false; @@ -56,7 +53,7 @@ export abstract class Disposable implements IDisposable { this._toDispose = dispose(this._toDispose); } - protected _register(t: T): T { + public push(t: T): T { if (this._lifecycle_disposable_isDisposed) { console.warn('Registering disposable on object that has already been disposed.'); t.dispose(); @@ -68,6 +65,21 @@ export abstract class Disposable implements IDisposable { } } +export abstract class Disposable implements IDisposable { + + static None = Object.freeze({ dispose() { } }); + + private readonly _store = new DisposableStore(); + + public dispose(): void { + this._store.dispose(); + } + + protected _register(t: T): T { + return this._store.push(t); + } +} + export interface IReference extends IDisposable { readonly object: T; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index e0abea919a8..735814fc0e1 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -64,12 +64,12 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews super(); this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); - _editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._toDispose); - _editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this, this._toDispose); + this._register(_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this)); + this._register(_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this)); // This reviver's only job is to activate webview extensions // This should trigger the real reviver to be registered from the extension host side. - this._toDispose.push(_webviewService.registerReviver({ + this._register(_webviewService.registerReviver({ canRevive: (webview) => { const viewType = webview.state.viewType; if (viewType) { @@ -80,9 +80,9 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews reviveWebview: () => { throw new Error('not implemented'); } })); - lifecycleService.onBeforeShutdown(e => { + this._register(lifecycleService.onBeforeShutdown(e => { e.veto(this._onBeforeShutdown()); - }, this, this._toDispose); + }, this)); } public $createWebviewPanel( diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 885211a42be..50995662333 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -47,7 +47,7 @@ export class NoTabsTitleControl extends TitleControl { // Breadcrumbs this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent }); toggleClass(this.titleContainer, 'breadcrumbs', Boolean(this.breadcrumbsControl)); - this.toDispose.push({ dispose: () => removeClass(this.titleContainer, 'breadcrumbs') }); // import to remove because the container is a shared dom node + this._register({ dispose: () => removeClass(this.titleContainer, 'breadcrumbs') }); // import to remove because the container is a shared dom node // Right Actions Container const actionsContainer = document.createElement('div'); diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index 7fa8f9d7873..08a3903f270 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -15,7 +15,7 @@ import { clamp } from 'vs/base/common/numbers'; import { Themable } from 'vs/workbench/common/theme'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDisposable, Disposable, combinedDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -160,7 +160,7 @@ class LargeImageView { DOM.clearNode(container); - const disposables: IDisposable[] = []; + const disposables = new DisposableStore(); const label = document.createElement('p'); label.textContent = nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size); @@ -175,7 +175,7 @@ class LargeImageView { disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternal(descriptor.resource))); } - return combinedDisposable(disposables); + return disposables; } } @@ -212,7 +212,7 @@ class FileSeemsBinaryFileView { DOM.clearNode(container); - const disposables: IDisposable[] = []; + const disposables = new DisposableStore(); const label = document.createElement('p'); label.textContent = nls.localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); @@ -228,7 +228,7 @@ class FileSeemsBinaryFileView { scrollbar.scanDomNode(); - return combinedDisposable(disposables); + return disposables; } } @@ -363,11 +363,11 @@ class InlineImageView { scrollbar: DomScrollableElement, delegate: ResourceViewerDelegate ) { - const disposables: IDisposable[] = []; + const disposables = new DisposableStore(); const context: ResourceViewerContext = { layout(dimension: DOM.Dimension) { }, - dispose: () => dispose(disposables) + dispose: () => disposables.dispose() }; const cacheKey = `${descriptor.resource.toString()}:${descriptor.etag}`; diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 72f669eda8a..75a4c0d4a08 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -6,24 +6,25 @@ import * as DOM from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { IAction } from 'vs/base/common/actions'; -import { Disposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenu } from 'vs/platform/actions/common/actions'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -export class CommentFormActions extends Disposable { +export class CommentFormActions { private _buttonElements: HTMLElement[] = []; + private _toDispose = new DisposableStore(); constructor( private container: HTMLElement, private actionHandler: (action: IAction) => void, private themeService: IThemeService - ) { - super(); - } + ) { } setActions(menu: IMenu) { - dispose(this._toDispose); + this._toDispose.dispose(); + this._toDispose = new DisposableStore(); + this._buttonElements.forEach(b => DOM.removeNode(b)); const groups = menu.getActions({ shouldForwardArgs: true }); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 8d9cfdc2585..56c2a8667f9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -178,8 +178,8 @@ export class CommentNode extends Disposable { let commentMenus = this.commentService.getCommentMenus(this.owner); const menu = commentMenus.getCommentTitleActions(this.comment, this._contextKeyService); - this._toDispose.push(menu); - this._toDispose.push(menu.onDidChange(e => { + this._register(menu); + this._register(menu.onDidChange(e => { const contributedActions = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], []); this.toolbar.setActions(contributedActions); })); @@ -217,7 +217,7 @@ export class CommentNode extends Disposable { this.registerActionBarListeners(this._actionsToolbarContainer); this.toolbar.setActions(actions, [])(); - this._toDispose.push(this.toolbar); + this._register(this.toolbar); } } @@ -354,7 +354,7 @@ export class CommentNode extends Disposable { return this.actionViewItemProvider(action as Action); } }); - this._toDispose.push(this._reactionsActionBar); + this._register(this._reactionsActionBar); this.comment.commentReactions!.map(reaction => { let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { @@ -444,8 +444,8 @@ export class CommentNode extends Disposable { })); } - this._toDispose.push(this._commentEditor); - this._toDispose.push(this._commentEditorModel); + this._register(this._commentEditor); + this._register(this._commentEditorModel); } private removeCommentEditor() { @@ -561,8 +561,8 @@ export class CommentNode extends Disposable { const menus = this.commentService.getCommentMenus(this.owner); const menu = menus.getCommentActions(this.comment, this._contextKeyService); - this._toDispose.push(menu); - this._toDispose.push(menu.onDidChange(() => { + this._register(menu); + this._register(menu.onDidChange(() => { this._commentFormActions.setActions(menu); })); @@ -599,17 +599,17 @@ export class CommentNode extends Disposable { const cancelEditButton = new Button(formActions); cancelEditButton.label = nls.localize('label.cancel', "Cancel"); - this._toDispose.push(attachButtonStyler(cancelEditButton, this.themeService)); + this._register(attachButtonStyler(cancelEditButton, this.themeService)); - this._toDispose.push(cancelEditButton.onDidClick(_ => { + this._register(cancelEditButton.onDidClick(_ => { this.removeCommentEditor(); })); this._updateCommentButton = new Button(formActions); this._updateCommentButton.label = UPDATE_COMMENT_LABEL; - this._toDispose.push(attachButtonStyler(this._updateCommentButton, this.themeService)); + this._register(attachButtonStyler(this._updateCommentButton, this.themeService)); - this._toDispose.push(this._updateCommentButton.onDidClick(_ => { + this._register(this._updateCommentButton.onDidClick(_ => { this.editComment(); })); @@ -622,21 +622,21 @@ export class CommentNode extends Disposable { } private registerActionBarListeners(actionsContainer: HTMLElement): void { - this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseenter', () => { + this._register(dom.addDisposableListener(this._domNode, 'mouseenter', () => { actionsContainer.classList.remove('hidden'); })); - this._toDispose.push(dom.addDisposableListener(this._domNode, 'focus', () => { + this._register(dom.addDisposableListener(this._domNode, 'focus', () => { actionsContainer.classList.remove('hidden'); })); - this._toDispose.push(dom.addDisposableListener(this._domNode, 'mouseleave', () => { + this._register(dom.addDisposableListener(this._domNode, 'mouseleave', () => { if (!this._domNode.contains(document.activeElement)) { actionsContainer.classList.add('hidden'); } })); - this._toDispose.push(dom.addDisposableListener(this._domNode, 'focusout', (e: FocusEvent) => { + this._register(dom.addDisposableListener(this._domNode, 'focusout', (e: FocusEvent) => { if (!this._domNode.contains((e.relatedTarget))) { actionsContainer.classList.add('hidden'); @@ -709,8 +709,4 @@ export class CommentNode extends Disposable { }, 3000); } } - - dispose() { - this._toDispose.forEach(disposeable => disposeable.dispose()); - } } \ No newline at end of file diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index f07ca7f2219..0e160e29040 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -199,9 +199,9 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { ) { super(null, action, [], -1, contextViewService, { ariaLabel: nls.localize('debugSession', 'Debug Session') }); - this.toDispose.push(attachSelectBoxStyler(this.selectBox, themeService)); + this._register(attachSelectBoxStyler(this.selectBox, themeService)); - this.toDispose.push(this.debugService.getViewModel().onDidFocusSession(() => { + this._register(this.debugService.getViewModel().onDidFocusSession(() => { const session = this.debugService.getViewModel().focusedSession; if (session) { const index = this.getSessions().indexOf(session); @@ -209,8 +209,8 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { } })); - this.toDispose.push(this.debugService.onDidNewSession(() => this.update())); - this.toDispose.push(this.debugService.onDidEndSession(() => this.update())); + this._register(this.debugService.onDidNewSession(() => this.update())); + this._register(this.debugService.onDidEndSession(() => this.update())); this.update(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 2f2d7c2ea09..688582fc043 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -112,7 +112,7 @@ export class DebugViewlet extends ViewContainerViewlet { if (!this.debugToolBarMenu) { this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); - this.toDispose.push(this.debugToolBarMenu); + this._register(this.debugToolBarMenu); } return DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index d2acc3d92b4..7c75e0b5ce4 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -381,7 +381,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati supportDynamicHeights: true }) as WorkbenchAsyncDataTree; - this.toDispose.push(this.tree.onContextMenu(e => this.onContextMenu(e))); + this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); // Make sure to select the session if debugging is already active this.selectSession(); this.styleElement = dom.createStyleSheet(this.container); diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts index 0beff3f7ec2..cf63a507426 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts @@ -97,14 +97,14 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { this.container = element; // Prevent showing dropdown on anything but left click - this.toDispose.push(addDisposableListener(this.container, 'mousedown', (e: MouseEvent) => { + this._register(addDisposableListener(this.container, 'mousedown', (e: MouseEvent) => { if (e.button !== 0) { EventHelper.stop(e, true); } }, true)); // Offer context menu to hide status bar entry - this.toDispose.push(addDisposableListener(this.container, 'contextmenu', e => { + this._register(addDisposableListener(this.container, 'contextmenu', e => { EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index bad977ca281..89e12070e37 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -348,7 +348,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } })); - this.tree.onContextMenu(this.onContextMenu, this, this._toDispose); + this._register(this.tree.onContextMenu(this.onContextMenu, this)); this._register(this.configurationService.onDidChangeConfiguration(e => { if (this.filterAction.useFilesExclude && e.affectsConfiguration('files.exclude')) { diff --git a/src/vs/workbench/contrib/output/browser/outputActions.ts b/src/vs/workbench/contrib/output/browser/outputActions.ts index a7d2ae2d3cc..d54b975b717 100644 --- a/src/vs/workbench/contrib/output/browser/outputActions.ts +++ b/src/vs/workbench/contrib/output/browser/outputActions.ts @@ -141,10 +141,10 @@ export class SwitchOutputActionViewItem extends SelectActionViewItem { super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', 'Output Channels.') }); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); - this.toDispose.push(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions())); - this.toDispose.push(outputChannelRegistry.onDidRemoveChannel(() => this.updateOtions())); - this.toDispose.push(this.outputService.onActiveOutputChannel(() => this.updateOtions())); - this.toDispose.push(attachSelectBoxStyler(this.selectBox, themeService)); + this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions())); + this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOtions())); + this._register(this.outputService.onActiveOutputChannel(() => this.updateOtions())); + this._register(attachSelectBoxStyler(this.selectBox, themeService)); this.updateOtions(); } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index caa82d3520f..1efc06338f6 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -1106,15 +1106,15 @@ export class SCMViewlet extends ViewContainerViewlet implements IViewModel { super(VIEWLET_ID, SCMViewlet.STATE_KEY, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); this.menus = instantiationService.createInstance(SCMMenus, undefined); - this.menus.onDidChangeTitle(this.updateTitleArea, this, this.toDispose); + this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this)); this.message = $('.empty-message', { tabIndex: 0 }, localize('no open repo', "No source control providers registered.")); - configurationService.onDidChangeConfiguration(e => { + this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.alwaysShowProviders')) { this.onDidChangeRepositories(); } - }, this.toDispose); + })); } create(parent: HTMLElement): void { @@ -1124,8 +1124,8 @@ export class SCMViewlet extends ViewContainerViewlet implements IViewModel { addClasses(parent, 'scm-viewlet', 'empty'); append(parent, this.message); - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.toDispose); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.toDispose); + this._register(this.scmService.onDidAddRepository(this.onDidAddRepository, this)); + this._register(this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this)); this.scmService.repositories.forEach(r => this.onDidAddRepository(r)); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 265f11a583f..371611936cc 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -509,7 +509,7 @@ export class WebviewElement extends Disposable implements Webview { } this.style(themeService.getTheme()); - themeService.onThemeChange(this.style, this, this._toDispose); + this._register(themeService.onThemeChange(this.style, this)); } public mountTo(parent: HTMLElement) {