From cab750ca8be061647c4e510f6bb1e8de87053df2 Mon Sep 17 00:00:00 2001 From: Ishan Taldekar Date: Fri, 14 Feb 2025 16:08:05 -0500 Subject: [PATCH] Support select all and copy shortcuts in chat (#353) * Support select all and copy keyboard shortcuts in chat * Fix submitted code block cancel button (#354) * Fix submitted code block cancel button * Fix empty overflow bubble (#356) * Fix empty pop-up when no overflow in follow up question text * Fix accidentally removed copy event listener --- .../amazonq/views/AmazonQChatWebview.java | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) 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 934a0c30..236c743f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.views; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; import java.util.List; import java.util.Optional; @@ -26,6 +28,8 @@ import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; 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.views.actions.AmazonQCommonActions; @@ -91,6 +95,7 @@ public void completed(final ProgressEvent event) { amazonQCommonActions = getAmazonQCommonActions(); chatCommunicationManager.setChatUiRequestListener(this); + new BrowserFunction(browser, "ideCommand") { @Override public Object function(final Object[] arguments) { @@ -101,6 +106,24 @@ public Object function(final Object[] arguments) { } }; + 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 @@ -210,6 +233,16 @@ private String generateCss() { mask-position: center; scale: 60%; } + .mynah-button.mynah-button-secondary.fill-state-always.code-snippet-close-button.mynah-ui-clickable-item + .mynah-ui-icon-cancel { + -webkit-mask-size: 187.5% !important; + mask-size: 187.5% !important; + mask-position: center; + aspect-ratio: 1/1; + width: 15px; + height: 15px; + scale: 50% + } .mynah-ui-icon-tabs { -webkit-mask-size: 102% !important; mask-size: 102% !important; @@ -272,7 +305,78 @@ private String generateJS(final String jsEntrypoint) { }); } }); - + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'a') { + textarea.select(); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + + window.addEventListener('load', () => { + const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); + if (textarea) { + textarea.addEventListener("keydown", (event) => { + if (((isMacOs() && event.metaKey) || (!isMacOs() && event.ctrlKey)) && event.key === 'c') { + copyToClipboard(textarea.value); + event.preventDefault(); + event.stopPropagation(); + } + }); + } + }); + + + const observer = new MutationObserver((mutations) => { + try { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1) { // Check if it's an element node + // Check for direct match + if (node.matches('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item')) { + attachEventListeners(node); + } + + // Check for nested matches + const buttons = node.querySelectorAll('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-chat-item-followup-question-option.mynah-ui-clickable-item'); + buttons.forEach(attachEventListeners); + } + }); + }); + } catch (error) { + console.error('Error in mutation observer:', error); + } + }); + + function attachEventListeners(element) { + if (!element || element.dataset.hasListener) return; // Prevent duplicate listeners + + element.addEventListener('mouseover', function(event) { + const textSpan = this.querySelector('span.mynah-button-label'); + if (textSpan && textSpan.scrollWidth <= textSpan.offsetWidth) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + }, true); + element.dataset.hasListener = 'true'; + } + + try { + observer.observe(document.body, { + childList: true, + subtree: true + }); + } catch (error) { + console.error('Error starting observer:', error); + } + """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, "true".equals(disclaimerAcknowledged)); }