From a4a0a5c13208d91d8244d63b09be62c1e6b5657b Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Thu, 27 Feb 2025 14:57:46 -0500 Subject: [PATCH] Refactor asset providers (#383) * Refactor asset provider to handle JS asset setup * Move progress listener for flicker bug out of asset providers * Pre-fetch assets to publish asset state on event broker --- .../amazonq/chat/ChatStateManager.java | 14 --- .../assets/ChatWebViewAssetProvider.java | 85 ++++++++++++++++++ .../ToolkitLoginWebViewAssetProvider.java | 38 ++++++++ .../assets/WebViewAssetProvider.java | 8 ++ .../providers/assets/WebViewAssetState.java | 13 --- .../amazonq/views/AmazonQChatWebview.java | 86 ++----------------- .../eclipse/amazonq/views/AmazonQView.java | 5 -- .../amazonq/views/ToolkitLoginWebview.java | 45 ++-------- 8 files changed, 142 insertions(+), 152 deletions(-) delete mode 100644 plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetState.java diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java index 4b467fe6..fa87c3c7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java @@ -3,26 +3,17 @@ package software.aws.toolkits.eclipse.amazonq.chat; -import java.util.Optional; - import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.PlatformUI; -import software.aws.toolkits.eclipse.amazonq.providers.assets.ChatWebViewAssetProvider; - public final class ChatStateManager { private static ChatStateManager instance; - private ChatWebViewAssetProvider chatAssetProvider; private Browser browser; private Composite dummyParent; private volatile boolean hasPreservedState = false; - private ChatStateManager() { - chatAssetProvider = new ChatWebViewAssetProvider(); - } - public static synchronized ChatStateManager getInstance() { if (instance == null) { instance = new ChatStateManager(); @@ -48,10 +39,6 @@ public synchronized void updateBrowser(final Browser browser) { this.browser = browser; } - public synchronized Optional getContent() { - return chatAssetProvider.getContent(); - } - public synchronized boolean hasPreservedState() { return hasPreservedState; } @@ -83,7 +70,6 @@ public void dispose() { browser = null; } disposeDummyParent(); - chatAssetProvider.dispose(); hasPreservedState = false; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ChatWebViewAssetProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ChatWebViewAssetProvider.java index 0561f3ec..dda4d9d6 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ChatWebViewAssetProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ChatWebViewAssetProvider.java @@ -3,14 +3,24 @@ package software.aws.toolkits.eclipse.amazonq.providers.assets; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.swt.browser.ProgressAdapter; +import org.eclipse.swt.browser.ProgressEvent; +import org.eclipse.swt.widgets.Display; + import com.fasterxml.jackson.databind.ObjectMapper; +import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; +import software.aws.toolkits.eclipse.amazonq.chat.ChatTheme; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys; import software.aws.toolkits.eclipse.amazonq.lsp.AwsServerCapabiltiesProvider; import software.aws.toolkits.eclipse.amazonq.lsp.model.ChatOptions; @@ -19,11 +29,77 @@ import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspManagerProvider; import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; +import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; +import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; +import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.util.WebviewAssetServer; +import software.aws.toolkits.eclipse.amazonq.views.AmazonQChatViewActionHandler; +import software.aws.toolkits.eclipse.amazonq.views.LoginViewCommandParser; +import software.aws.toolkits.eclipse.amazonq.views.ViewActionHandler; +import software.aws.toolkits.eclipse.amazonq.views.ViewCommandParser; public final class ChatWebViewAssetProvider extends WebViewAssetProvider { private WebviewAssetServer webviewAssetServer; + private final ChatTheme chatTheme; + private final ViewCommandParser commandParser; + private final ViewActionHandler actionHandler; + private final ChatCommunicationManager chatCommunicationManager; + + public ChatWebViewAssetProvider() { + chatTheme = new ChatTheme(); + commandParser = new LoginViewCommandParser(); + chatCommunicationManager = ChatCommunicationManager.getInstance(); + actionHandler = new AmazonQChatViewActionHandler(chatCommunicationManager); + } + + @Override + public void injectAssets(final Browser browser) { + new BrowserFunction(browser, "ideCommand") { + @Override + public Object function(final Object[] arguments) { + ThreadingUtils.executeAsyncTask(() -> { + handleMessageFromUI(browser, arguments); + }); + return null; + } + }; + + new BrowserFunction(browser, "isMacOs") { + @Override + public Object function(final Object[] arguments) { + return Boolean.TRUE.equals(PluginUtils.getPlatform() == PluginPlatform.MAC); + } + }; + + new BrowserFunction(browser, "copyToClipboard") { + @Override + public Object function(final Object[] arguments) { + if (arguments.length > 0 && arguments[0] instanceof String) { + StringSelection stringSelection = new StringSelection((String) arguments[0]); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); + } + return null; + } + }; + + // Inject chat theme after mynah-ui has loaded + browser.addProgressListener(new ProgressAdapter() { + @Override + public void completed(final ProgressEvent event) { + Display.getDefault().syncExec(() -> { + try { + chatTheme.injectTheme(browser); + disableBrowserContextMenu(browser); + } catch (Exception e) { + Activator.getLogger().info("Error occurred while injecting theme into Q chat", e); + } + }); + } + }); + + browser.setText(getContent().get()); + } @Override public Optional getContent() { @@ -298,6 +374,15 @@ private String serializeQuickActionCommands(final List } } + private void handleMessageFromUI(final Browser browser, final Object[] arguments) { + try { + commandParser.parseCommand(arguments) + .ifPresent(parsedCommand -> actionHandler.handleCommand(parsedCommand, browser)); + } catch (Exception e) { + Activator.getLogger().error("Error processing message from Amazon Q chat", e); + } + } + public Optional resolveJsPath() { var chatUiDirectory = getChatUiDirectory(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ToolkitLoginWebViewAssetProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ToolkitLoginWebViewAssetProvider.java index ec24e516..c9ad4f1f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ToolkitLoginWebViewAssetProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/ToolkitLoginWebViewAssetProvider.java @@ -11,15 +11,53 @@ import java.nio.file.Paths; import java.util.Optional; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; + import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; import software.aws.toolkits.eclipse.amazonq.util.WebviewAssetServer; +import software.aws.toolkits.eclipse.amazonq.views.LoginViewActionHandler; +import software.aws.toolkits.eclipse.amazonq.views.LoginViewCommandParser; +import software.aws.toolkits.eclipse.amazonq.views.ViewActionHandler; +import software.aws.toolkits.eclipse.amazonq.views.ViewCommandParser; +import software.aws.toolkits.eclipse.amazonq.views.ViewConstants; public final class ToolkitLoginWebViewAssetProvider extends WebViewAssetProvider { private WebviewAssetServer webviewAssetServer; private static final ThemeDetector THEME_DETECTOR = new ThemeDetector(); + private final ViewCommandParser commandParser; + private final ViewActionHandler actionHandler; + + public ToolkitLoginWebViewAssetProvider() { + this.commandParser = new LoginViewCommandParser(); + this.actionHandler = new LoginViewActionHandler(); + } + + @Override + public void injectAssets(final Browser browser) { + new BrowserFunction(browser, ViewConstants.COMMAND_FUNCTION_NAME) { + @Override + public Object function(final Object[] arguments) { + commandParser.parseCommand(arguments) + .ifPresent(command -> actionHandler.handleCommand(command, browser)); + return null; + } + }; + new BrowserFunction(browser, "telemetryEvent") { + @Override + public Object function(final Object[] arguments) { + String clickEvent = (String) arguments[0]; + UiTelemetryProvider.emitClickEventMetric("auth_" + clickEvent); + return null; + } + }; + + browser.setText(getContent().get()); + } @Override public Optional getContent() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetProvider.java index e7113776..4acc7703 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetProvider.java @@ -5,8 +5,12 @@ import java.util.Optional; +import org.eclipse.swt.browser.Browser; + public abstract class WebViewAssetProvider { + public abstract void injectAssets(final Browser browser); + public abstract Optional getContent(); public abstract void dispose(); @@ -31,4 +35,8 @@ function waitForFunction(functionName, timeout = 30000) { """; } + protected final void disableBrowserContextMenu(final Browser browser) { + browser.execute("document.oncontextmenu = e => e.preventDefault();"); + } + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetState.java deleted file mode 100644 index c6971e16..00000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/assets/WebViewAssetState.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.eclipse.amazonq.providers.assets; - -public enum WebViewAssetState { - RESOLVED, DEPENDENCY_MISSING; - - public boolean isDependencyMissing() { - return this == DEPENDENCY_MISSING; - } - -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index fff2a7fe..849609ff 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -3,12 +3,7 @@ package software.aws.toolkits.eclipse.amazonq.views; -import java.awt.Toolkit; -import java.awt.datatransfer.StringSelection; -import java.util.Optional; - import org.eclipse.swt.browser.Browser; -import org.eclipse.swt.browser.BrowserFunction; import org.eclipse.swt.browser.ProgressAdapter; import org.eclipse.swt.browser.ProgressEvent; import org.eclipse.swt.widgets.Composite; @@ -16,13 +11,8 @@ import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; import software.aws.toolkits.eclipse.amazonq.chat.ChatStateManager; -import software.aws.toolkits.eclipse.amazonq.chat.ChatTheme; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.providers.assets.ChatWebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.providers.assets.WebViewAssetProvider; -import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; -import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; -import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions; public class AmazonQChatWebview extends AmazonQView implements ChatUiRequestListener { @@ -31,24 +21,17 @@ public class AmazonQChatWebview extends AmazonQView implements ChatUiRequestList private AmazonQCommonActions amazonQCommonActions; private final ChatStateManager chatStateManager; - private final ViewCommandParser commandParser; - private final ViewActionHandler actionHandler; private final ChatCommunicationManager chatCommunicationManager; - private final ChatTheme chatTheme; private Browser browser; private volatile boolean canDisposeState = false; private WebViewAssetProvider webViewAssetProvider; - private Optional content; public AmazonQChatWebview() { super(); - this.chatStateManager = ChatStateManager.getInstance(); - this.commandParser = new LoginViewCommandParser(); - this.chatCommunicationManager = ChatCommunicationManager.getInstance(); - this.actionHandler = new AmazonQChatViewActionHandler(chatCommunicationManager); - this.webViewAssetProvider = new ChatWebViewAssetProvider(); - this.content = this.webViewAssetProvider.getContent(); - this.chatTheme = new ChatTheme(); + chatStateManager = ChatStateManager.getInstance(); + chatCommunicationManager = ChatCommunicationManager.getInstance(); + webViewAssetProvider = new ChatWebViewAssetProvider(); + webViewAssetProvider.getContent(); } @Override @@ -80,10 +63,7 @@ public void completed(final ProgressEvent event) { } }); - if (!content.isPresent()) { - content = webViewAssetProvider.getContent(); - } - browser.setText(content.get()); + webViewAssetProvider.injectAssets(browser); } else { updateBrowser(browser); } @@ -95,53 +75,8 @@ public void completed(final ProgressEvent event) { chatCommunicationManager.setChatUiRequestListener(this); - new BrowserFunction(browser, "ideCommand") { - @Override - public Object function(final Object[] arguments) { - ThreadingUtils.executeAsyncTask(() -> { - handleMessageFromUI(browser, arguments); - }); - return null; - } - }; - - addFocusListener(parent, browser); - - new BrowserFunction(browser, "isMacOs") { - @Override - public Object function(final Object[] arguments) { - return Boolean.TRUE.equals(PluginUtils.getPlatform() == PluginPlatform.MAC); - } - }; - - new BrowserFunction(browser, "copyToClipboard") { - @Override - public Object function(final Object[] arguments) { - if (arguments.length > 0 && arguments[0] instanceof String) { - StringSelection stringSelection = new StringSelection((String) arguments[0]); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); - } - return null; - } - }; addFocusListener(parent, browser); - - // Inject chat theme after mynah-ui has loaded - browser.addProgressListener(new ProgressAdapter() { - @Override - public void completed(final ProgressEvent event) { - Display.getDefault().syncExec(() -> { - try { - chatTheme.injectTheme(browser); - disableBrowserContextMenu(); - } catch (Exception e) { - Activator.getLogger().info("Error occurred while injecting theme into Q chat", e); - } - }); - } - }); - setupAmazonQCommonActions(); return parent; @@ -153,15 +88,6 @@ private Browser getAndUpdateStateManager() { return browser; } - private void handleMessageFromUI(final Browser browser, final Object[] arguments) { - try { - commandParser.parseCommand(arguments) - .ifPresent(parsedCommand -> actionHandler.handleCommand(parsedCommand, browser)); - } catch (Exception e) { - Activator.getLogger().error("Error processing message from Amazon Q chat", e); - } - } - @Override public final void onSendToChatUi(final String message) { String script = "window.postMessage(" + message + ");"; @@ -180,9 +106,7 @@ public final void dispose() { if (canDisposeState) { ChatStateManager.getInstance().dispose(); webViewAssetProvider.dispose(); - content = Optional.empty(); } - webViewAssetProvider.dispose(); super.dispose(); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java index 2024f48c..3a2a60f1 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java @@ -63,16 +63,11 @@ public Composite setupView(final Composite parent) { if (browser != null && !browser.isDisposed()) { setupBrowserBackground(parent); - disableBrowserContextMenu(); } return parent; } - protected final void disableBrowserContextMenu() { - getBrowser().execute("document.oncontextmenu = e => e.preventDefault();"); - } - private void setupBrowserBackground(final Composite parent) { var bgColor = parent.getBackground(); getBrowser().setBackground(bgColor); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java index 5b3499ca..91735bac 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java @@ -3,9 +3,6 @@ package software.aws.toolkits.eclipse.amazonq.views; -import java.util.Optional; - -import org.eclipse.swt.browser.BrowserFunction; import org.eclipse.swt.browser.ProgressAdapter; import org.eclipse.swt.browser.ProgressEvent; import org.eclipse.swt.widgets.Composite; @@ -13,7 +10,6 @@ import software.aws.toolkits.eclipse.amazonq.providers.assets.ToolkitLoginWebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.providers.assets.WebViewAssetProvider; -import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions; public final class ToolkitLoginWebview extends AmazonQView { @@ -23,18 +19,13 @@ public final class ToolkitLoginWebview extends AmazonQView { private AmazonQCommonActions amazonQCommonActions; private final WebViewAssetProvider webViewAssetProvider; - private final ViewCommandParser commandParser; - private final ViewActionHandler actionHandler; - private Optional content; private boolean isViewVisible = false; public ToolkitLoginWebview() { super(); - this.commandParser = new LoginViewCommandParser(); - this.actionHandler = new LoginViewActionHandler(); - this.webViewAssetProvider = new ToolkitLoginWebViewAssetProvider(); - this.content = this.webViewAssetProvider.getContent(); + webViewAssetProvider = new ToolkitLoginWebViewAssetProvider(); + webViewAssetProvider.getContent(); } @Override @@ -49,46 +40,22 @@ public Composite setupView(final Composite parent) { } var browser = getBrowser(); - addFocusListener(parent, browser); - - browser.setVisible(isViewVisible); + browser.setVisible(false); browser.addProgressListener(new ProgressAdapter() { @Override public void completed(final ProgressEvent event) { Display.getDefault().asyncExec(() -> { if (!browser.isDisposed()) { - isViewVisible = true; - browser.setVisible(isViewVisible); + browser.setVisible(true); } }); } }); - new BrowserFunction(browser, ViewConstants.COMMAND_FUNCTION_NAME) { - @Override - public Object function(final Object[] arguments) { - commandParser.parseCommand(arguments) - .ifPresent(command -> actionHandler.handleCommand(command, browser)); - return null; - } - }; - new BrowserFunction(browser, "telemetryEvent") { - @Override - public Object function(final Object[] arguments) { - String clickEvent = (String) arguments[0]; - UiTelemetryProvider.emitClickEventMetric("auth_" + clickEvent); - return null; - } - }; + webViewAssetProvider.injectAssets(browser); + addFocusListener(parent, browser); amazonQCommonActions = getAmazonQCommonActions(); - browser.setText(webViewAssetProvider.getContent().get()); - - if (!content.isPresent()) { - content = webViewAssetProvider.getContent(); - } - - browser.setText(content.get()); setupAmazonQCommonActions();