From 3c74b287473db71db34f5e06acfa3b2bd4d7c1a8 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 31 Mar 2025 11:18:08 -0400 Subject: [PATCH] Ensure only one reload extension dialog can be shown at a time If `showReloadExtensionNotification` is in flight and being awaited then return the promise being awaited instead of showing another dialog. Also, debounce `showReloadExtensionNotification` with a 5 second window because its possible for users to close the first dialog very fast and have another one appear immediately if we update several settings at a time that require an extension restart. Issue: #1471 --- src/ui/ReloadExtension.ts | 48 +++++++++++++++++----- test/unit-tests/ui/ReloadExtension.test.ts | 25 ++++++++++- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/ui/ReloadExtension.ts b/src/ui/ReloadExtension.ts index b8d201589..a23de1628 100644 --- a/src/ui/ReloadExtension.ts +++ b/src/ui/ReloadExtension.ts @@ -14,23 +14,49 @@ import * as vscode from "vscode"; import { Workbench } from "../utilities/commands"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import debounce = require("lodash.debounce"); /** * Prompts the user to reload the extension in cases where we are unable to do - * so automatically. + * so automatically. Only one of these prompts will be shown at a time. * * @param message the warning message to display to the user * @param items extra buttons to display * @returns the selected button or undefined if cancelled */ -export async function showReloadExtensionNotification( - message: string, - ...items: T[] -): Promise<"Reload Extensions" | T | undefined> { - const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; - const selected = await vscode.window.showWarningMessage(message, ...buttons); - if (selected === "Reload Extensions") { - await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); - } - return selected; +export function showReloadExtensionNotificationInstance() { + let inFlight: Promise<"Reload Extensions" | T | undefined> | null = null; + + return async function ( + message: string, + ...items: T[] + ): Promise<"Reload Extensions" | T | undefined> { + if (inFlight) { + return inFlight; + } + + const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; + inFlight = (async () => { + try { + const selected = await vscode.window.showWarningMessage(message, ...buttons); + if (selected === "Reload Extensions") { + await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); + } + return selected; + } finally { + inFlight = null; + } + })(); + + return inFlight; + }; } + +// In case the user closes the dialog immediately we want to debounce showing it again +// for 10 seconds to prevent another popup perhaps immediately appearing. +export const showReloadExtensionNotification = debounce( + showReloadExtensionNotificationInstance(), + 10_000, + { leading: true } +); diff --git a/test/unit-tests/ui/ReloadExtension.test.ts b/test/unit-tests/ui/ReloadExtension.test.ts index 422bfce23..8c539b062 100644 --- a/test/unit-tests/ui/ReloadExtension.test.ts +++ b/test/unit-tests/ui/ReloadExtension.test.ts @@ -11,15 +11,24 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import { beforeEach } from "mocha"; import { expect } from "chai"; import { mockGlobalObject } from "../../MockUtils"; import * as vscode from "vscode"; -import { showReloadExtensionNotification } from "../../../src/ui/ReloadExtension"; +import { showReloadExtensionNotificationInstance } from "../../../src/ui/ReloadExtension"; import { Workbench } from "../../../src/utilities/commands"; suite("showReloadExtensionNotification()", async function () { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); const mockedVSCodeCommands = mockGlobalObject(vscode, "commands"); + let showReloadExtensionNotification: ( + message: string, + ...items: string[] + ) => Promise; + + beforeEach(() => { + showReloadExtensionNotification = showReloadExtensionNotificationInstance(); + }); test("displays a warning message asking the user if they would like to reload the window", async () => { mockedVSCodeWindow.showWarningMessage.resolves(undefined); @@ -57,4 +66,18 @@ suite("showReloadExtensionNotification()", async function () { ); expect(mockedVSCodeCommands.executeCommand).to.not.have.been.called; }); + + test("only shows one dialog at a time", async () => { + mockedVSCodeWindow.showWarningMessage.resolves(undefined); + + await Promise.all([ + showReloadExtensionNotification("Want to reload?"), + showReloadExtensionNotification("Want to reload?"), + ]); + + expect(mockedVSCodeWindow.showWarningMessage).to.have.been.calledOnceWithExactly( + "Want to reload?", + "Reload Extensions" + ); + }); });