diff --git a/.gitignore b/.gitignore index 24d951fa5..5bf0f3eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ telemetry/bin bin package-lock.json telemetry/ +plugin/.settings \ No newline at end of file diff --git a/README.md b/README.md index 9cf54c071..89f86a26f 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,110 @@ ## Amazon Q for Eclipse -Amazon Q Developer helps users build faster across the entire software development lifecycle by providing tailored responses and code recommendations that conform to their team's internal libraries, proprietary algorithmic techniques, and enterprise code style. +Amazon Q Developer is an advanced AI-powered coding assistant designed to enhance developer productivity and streamline software development processes. -### Getting Started +### Key Features -* **Free Tier** - create or log in with an AWS Builder ID (a personal profile from AWS). -* **Pro Tier** - if your organization is on the Amazon Q Developer Pro tier, log in with single sign-on. +1. **Intelligent Code Generation & Assistance** + - Explains code and answers software development questions + - Generates real-time code suggestions from snippets to full functions + - Provides inline code suggestions based on your comments and existing code + - Supports contextual conversations about your code -### Features +2. **AI-Powered Development Agents** + - Automates complex, multistep tasks including: + - Unit testing + - Documentation + - Code reviews + - Assists with implementing features, documenting code, and bootstrapping new projects + - Achieved highest scores on the SWE-Bench Leaderboard and Leaderboard Lite -* Code faster with inline code suggestions - Amazon Q Developer generates real-time code suggestions ranging from snippets to full functions based on your comments and existing code. It also supports CLI completions and natural language–to-bash translation in the command line. -* Get assistance - Amazon Q Developer can generate code, explain code, and provide answers about software development. -* Customize code recommendations - Securely connect Amazon Q Developer to your private repositories to generate even more relevant code recommendations, ask questions about your company code, and understand your internal code bases faster. -* Code reference log - Attribute code from Amazon Q that is similar to training data. When code suggestions similar to training data are accepted, they will be added to the code reference log. +3. **Secure Private Repository Integration** + - Connects securely to your private repositories + - Customizes and generates more relevant code recommendations + - Enables querying about your company-specific code + - Accelerates understanding of internal code bases -### How to install -* **Step 1:** Inside Eclipse, select `Help -> Install New Software...`. -image +4. **Code Reference Log** + - Attributes code suggestions that are similar to training data + - Automatically logs accepted code suggestions that match training data + - Maintains transparency in AI-generated code origins -* **Step 2:** Add the Amazon Q for Eclipse update site URL `https://amazonq.eclipsetoolkit.amazonwebservices.com/` and select the plugin before continuing. -Screenshot 2024-11-26 at 9 24 55 AM +Amazon Q Developer is designed to be your intelligent coding companion, helping you write better code faster and understand complex codebases more efficiently. -* **Step 3:** Review the installation and license details and continue through to install the plugin. Upon completion you will be prompted to restart the IDE to finish the installation. -image +### Available Plans -### Screenshots -![login](https://github.com/user-attachments/assets/5345e2fb-fa43-469f-92b6-b388577077a6) -![inline](https://github.com/user-attachments/assets/e7a684f9-c568-4c63-a510-1fb85bef52e3) -![chat](https://github.com/user-attachments/assets/459592ac-2bef-416c-8430-28584a6d709f) +1. [Free Tier](https://aws.amazon.com/q/developer/getting-started/) + - Code faster with code suggestions in the IDE + - Review code licenses with reference tracking + - Limited monthly access to advanced features: + - Chat, debug code, add tests, and more in your IDE (50 interactions/month) + - Amazon Q Developer agents for software development (10 uses/month) + - Amazon Q Developer Agent for code transformation (1,000 lines of submitted code/month) + - Answers about your AWS account resources (25 queries/month) + +2. [Pro Tier](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-pro-tier-setting-up-access.html) + - Manage users and policies with enterprise access controls + - Customize Amazon Q to your code base for enhanced suggestions + - Increased limits on advanced features: + - Unlimited chat, debugging, and testing in your IDE + - Unlimited use of Amazon Q Developer agents for software development + - Unlimited answers about your AWS account resources + - Additional enterprise-grade features + +Both tiers continue to receive updates and improvements. + +### Installation Instructions + +#### Option 1: Install via Eclipse Marketplace (Recommended) +1. **Open Eclipse Marketplace** + - Launch Eclipse IDE + - Navigate to `Help > Eclipse Marketplace...` + - Search for "Amazon Q" + - Click "Install" on the Amazon Q plugin + +2. **Complete Installation** + - Review the installation details + - Accept the license agreement + - Click "Finish" + - When prompted, restart Eclipse to complete the installation + +![Install-Using-Marketplace](https://github.com/user-attachments/assets/dcb77afd-4ee0-4da7-adb0-dbc22cd80f70) + +#### Option 2: Install via Update Site +1. **Open Install Dialog** + - Launch Eclipse IDE + - Navigate to `Help > Install New Software...` + +2. **Add Amazon Q Repository** + - Click "Add..." button + - Enter the following details: + - Name: `Amazon Q for Eclipse` + - URL: `https://amazonq.eclipsetoolkit.amazonwebservices.com/` + - Click "Add" + - Select "software.aws.toolkits.eclipse" from the available software list + - Click "Next" + +3. **Complete Installation** + - Review the installation details + - Accept the license agreement + - Click "Finish" + - When prompted, restart Eclipse to complete the installation + +![Install-Using-Update-Site](https://github.com/user-attachments/assets/3d8e0667-a405-4daf-9736-dbcc254a3344) + +### Demos & Examples + +#### Code Context +![Explaining-A-Class](https://github.com/user-attachments/assets/86ff704b-8be1-41e0-be91-fc172b40478f) + +#### Generate Tests +![Test-Generation](https://github.com/user-attachments/assets/8f3edc09-6981-4bb2-b0bd-0201e6b73cf1) + +#### Create Documentation +![Inline-Chat](https://github.com/user-attachments/assets/ee867bcf-ef62-4468-86d3-1df53ddabf1b) + +#### Code Completion +![Code-Completion-Example](https://github.com/user-attachments/assets/30b76708-1cd4-4cd1-9abe-22c9a4a6a8bc) ## License diff --git a/feature/feature.xml b/feature/feature.xml index c1599f5b9..77584ce73 100644 --- a/feature/feature.xml +++ b/feature/feature.xml @@ -2,7 +2,7 @@ + version="2.0.0.qualifier"> Amazon Q Developer helps users build faster across the entire software development lifecycle by providing tailored responses and code recommendations that conform to their team's internal libraries, proprietary algorithmic techniques, and enterprise code style. @@ -198,6 +198,6 @@ https://github.com/aws/amazon-q-eclipse/blob/main/attribution.xml id="amazon-q-eclipse" download-size="11000" install-size="0" - version="1.1.0.qualifier" + version="2.0.0.qualifier" unpack="false"/> diff --git a/feature/pom.xml b/feature/pom.xml index eeb1542f7..dd42ca0c7 100644 --- a/feature/pom.xml +++ b/feature/pom.xml @@ -6,7 +6,7 @@ software.aws.toolkits.eclipse amazon-q-eclipse-group - 1.1.0-SNAPSHOT + 2.0.0-SNAPSHOT ../ diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index 24910ff6e..8cce1e99f 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -4,7 +4,7 @@ Bundle-Name: Amazon Q for Eclipse Bundle-Provider: Amazon Web Services Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-SymbolicName: amazon-q-eclipse;singleton:=true -Bundle-Version: 1.1.0.qualifier +Bundle-Version: 2.0.0.qualifier Automatic-Module-Name: amazon.q.eclipse Bundle-ActivationPolicy: lazy Bundle-Activator: software.aws.toolkits.eclipse.amazonq.plugin.Activator @@ -12,6 +12,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", org.tukaani.xz;bundle-version="1.9.0", org.eclipse.ui;bundle-version="3.205.100", org.eclipse.core.resources;bundle-version="3.20.100", + org.eclipse.core.filesystem;bundle-version="1.10.400.v20240426-1040", org.eclipse.jface.text;bundle-version="3.25.100", org.eclipse.jdt.ui;bundle-version="3.32.100", org.eclipse.ui.genericeditor;bundle-version="1.3.400", @@ -28,6 +29,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0", org.apache.commons.logging;bundle-version="1.2.0", slf4j.api;bundle-version="2.0.13", org.apache.commons.lang3;bundle-version="3.14.0", + org.apache.commons.text;bundle-version="1.10.0", org.eclipse.core.expressions;bundle-version="3.9.400", org.eclipse.jgit;bundle-version="6.10.0" Bundle-Classpath: ., diff --git a/plugin/checkstyle.xml b/plugin/checkstyle.xml index d90bd57ae..79ad80f83 100644 --- a/plugin/checkstyle.xml +++ b/plugin/checkstyle.xml @@ -15,6 +15,12 @@ + + + + + + diff --git a/plugin/copyright-header.txt b/plugin/copyright-header.txt new file mode 100644 index 000000000..920550255 --- /dev/null +++ b/plugin/copyright-header.txt @@ -0,0 +1,2 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/plugin/plugin.xml b/plugin/plugin.xml index d0f7f82d6..371be6311 100644 --- a/plugin/plugin.xml +++ b/plugin/plugin.xml @@ -160,6 +160,14 @@ + + + + + + @@ -196,6 +209,10 @@ class="org.eclipse.lsp4e.ConnectDocumentToLanguageServerSetupParticipant" contentTypeId="org.eclipse.wst.jsdt.core.jsSource"> + + @@ -550,7 +567,8 @@ - + + diff --git a/plugin/pom.xml b/plugin/pom.xml index 4a938dd21..0882b84dd 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -6,7 +6,7 @@ software.aws.toolkits.eclipse amazon-q-eclipse-group - 1.1.0-SNAPSHOT + 2.0.0-SNAPSHOT ../ @@ -14,12 +14,12 @@ eclipse-plugin - 2.28.26 + 2.31.41 v20.9.0 10.1.0 2.17.3 17 - 5.11.3 + 5.11.4 @@ -45,7 +45,7 @@ io.reactivex.rxjava3 rxjava - 3.1.5 + 3.1.10 jakarta.inject @@ -61,10 +61,15 @@ httpclient 4.5.14 + + org.apache.commons + commons-text + 1.10.0 + commons-codec commons-codec - 1.17.1 + 1.17.2 software.amazon.awssdk @@ -123,10 +128,10 @@ test - io.github.java-diff-utils - java-diff-utils - 4.15 - + io.github.java-diff-utils + java-diff-utils + 4.15 + @@ -166,7 +171,7 @@ maven-dependency-plugin - 3.8.0 + 3.8.1 copy-dependencies @@ -257,7 +262,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.1 + 3.5.3 test @@ -273,7 +278,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 compiletests @@ -287,12 +292,12 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.5.0 + 3.6.0 com.puppycrawl.tools checkstyle - 10.18.2 + 10.23.1 @@ -314,7 +319,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.13 default-prepare-agent diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/AmazonQLspState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/AmazonQLspState.java index f8770bd1d..806b8c7aa 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/AmazonQLspState.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/AmazonQLspState.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.broker.events; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/QDeveloperProfileState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/QDeveloperProfileState.java new file mode 100644 index 000000000..c8af5379f --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/QDeveloperProfileState.java @@ -0,0 +1,8 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.broker.events; + +public enum QDeveloperProfileState { + NOT_APPLICABLE, SELECTED, AVAILABLE +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/ViewRouterPluginState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/ViewRouterPluginState.java index 34a69ff3b..9a5e077fe 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/ViewRouterPluginState.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/events/ViewRouterPluginState.java @@ -6,5 +6,6 @@ import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; public record ViewRouterPluginState(AuthState authState, AmazonQLspState lspState, BrowserCompatibilityState browserCompatibilityState, - ChatWebViewAssetState chatWebViewAssetState, ToolkitLoginWebViewAssetState toolkitLoginWebViewAssetState) { + ChatWebViewAssetState chatWebViewAssetState, ToolkitLoginWebViewAssetState toolkitLoginWebViewAssetState, + QDeveloperProfileState qDeveloperProfileState) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatAsyncResultManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatAsyncResultManager.java new file mode 100644 index 000000000..4ef16e6e6 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatAsyncResultManager.java @@ -0,0 +1,86 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public final class ChatAsyncResultManager { + private static ChatAsyncResultManager instance; + private Map> results; + private Map completedResults; + private final long defaultTimeout; + private final TimeUnit defaultTimeUnit; + + private ChatAsyncResultManager(final long timeout, final TimeUnit timeUnit) { + results = new ConcurrentHashMap<>(); + completedResults = new ConcurrentHashMap<>(); + this.defaultTimeout = timeout; + this.defaultTimeUnit = timeUnit; + } + + public static synchronized ChatAsyncResultManager getInstance() { + if (instance == null) { + instance = new ChatAsyncResultManager(30, TimeUnit.SECONDS); + } + return instance; + } + + public void createRequestId(final String requestId) { + if (!completedResults.containsKey(requestId)) { + results.put(requestId, new CompletableFuture<>()); + } + } + + public void removeRequestId(final String requestId) { + CompletableFuture future = results.remove(requestId); + if (future != null && !future.isDone()) { + future.cancel(true); + } + completedResults.remove(requestId); + } + + public void setResult(final String requestId, final Object result) { + CompletableFuture future = results.get(requestId); + if (future != null) { + future.complete(result); + completedResults.put(requestId, result); + results.remove(requestId); + } else { + completedResults.put(requestId, result); + results.remove(requestId); + } + } + + + public Object getResult(final String requestId) throws Exception { + return getResult(requestId, defaultTimeout, defaultTimeUnit); + } + + private Object getResult(final String requestId, final long timeout, final TimeUnit unit) throws Exception { + Object completedResult = completedResults.get(requestId); + if (completedResult != null) { + return completedResult; + } + + CompletableFuture future = results.get(requestId); + if (future == null) { + throw new IllegalArgumentException("Request ID not found: " + requestId); + } + + try { + Object result = future.get(timeout, unit); + completedResults.put(requestId, result); + results.remove(requestId); + return result; + } catch (TimeoutException e) { + future.cancel(true); + results.remove(requestId); + throw new TimeoutException("Operation timed out for requestId: " + requestId); + } + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java index 0f86a4fe8..8384bf90e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java @@ -4,37 +4,44 @@ package software.aws.toolkits.eclipse.amazonq.chat; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.swt.widgets.Display; -import software.aws.toolkits.eclipse.amazonq.chat.models.BaseChatRequestParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.ChatRequestParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.ChatResult; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; +import software.aws.toolkits.eclipse.amazonq.chat.models.ButtonClickResult; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommand; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommandName; import software.aws.toolkits.eclipse.amazonq.chat.models.CursorState; import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; import software.aws.toolkits.eclipse.amazonq.chat.models.ErrorParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.InlineChatRequestParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.QuickActionParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.ReferenceTrackerInformation; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; import software.aws.toolkits.eclipse.amazonq.lsp.encryption.DefaultLspEncryptionManager; import software.aws.toolkits.eclipse.amazonq.lsp.encryption.LspEncryptionManager; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.JsonHandler; +import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; import software.aws.toolkits.eclipse.amazonq.util.ProgressNotificationUtils; import software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; @@ -49,120 +56,204 @@ * webview used for displaying chat conversations. It is implemented as a * singleton to centralize control of all communication in the plugin. */ -public final class ChatCommunicationManager { - private static ChatCommunicationManager instance; +public final class ChatCommunicationManager implements EventObserver { + private static volatile ChatCommunicationManager instance; private final JsonHandler jsonHandler; - private final CompletableFuture chatMessageProvider; private final ChatPartialResultMap chatPartialResultMap; private final LspEncryptionManager lspEncryptionManager; + + private final BlockingQueue commandQueue; + + private final Map lastProcessedTimeMap = new ConcurrentHashMap<>(); + + private static final int MINIMUM_PARTIAL_RESPONSE_LENGTH = 50; + private static final int MIN_DELAY_BETWEEN_PARTIALS = 500; + private static final int MAX_DELAY_BETWEEN_PARTIALS = 2500; + private static final int CHAR_COUNT_FOR_MAX_DELAY = 5000; + + private final ConcurrentHashMap partialResultLocks = new ConcurrentHashMap<>(); + private final ConcurrentHashMap finalResultProcessed = new ConcurrentHashMap<>(); + private CompletableFuture chatUiRequestListenerFuture; private CompletableFuture inlineChatListenerFuture; + private Map> inflightRequestByTabId = new ConcurrentHashMap>(); + + private volatile boolean isActive = false; + private volatile boolean isQueueProcessorRunning = false; + private volatile Thread queueProcessorThread; + private final String inlineChatTabId = "123456789"; private ChatCommunicationManager(final Builder builder) { this.jsonHandler = builder.jsonHandler != null ? builder.jsonHandler : new JsonHandler(); - this.chatMessageProvider = builder.chatMessageProvider != null ? builder.chatMessageProvider - : ChatMessageProvider.createAsync(); this.chatPartialResultMap = builder.chatPartialResultMap != null ? builder.chatPartialResultMap : new ChatPartialResultMap(); this.lspEncryptionManager = builder.lspEncryptionManager != null ? builder.lspEncryptionManager : DefaultLspEncryptionManager.getInstance(); chatUiRequestListenerFuture = new CompletableFuture<>(); inlineChatListenerFuture = new CompletableFuture<>(); + commandQueue = new LinkedBlockingQueue<>(); + Activator.getEventBroker().subscribe(ChatUIInboundCommand.class, this); } public static Builder builder() { return new Builder(); } - public static synchronized ChatCommunicationManager getInstance() { + public static ChatCommunicationManager getInstance() { if (instance == null) { - instance = ChatCommunicationManager.builder().build(); + synchronized (ChatCommunicationManager.class) { + if (instance == null) { + instance = ChatCommunicationManager.builder().build(); + } + } } return instance; } - public void sendMessageToChatServer(final Command command, final Object params) { - chatMessageProvider.thenAcceptAsync(chatMessageProvider -> { + public void sendMessageToChatServer(final Command command, final ChatMessage message) { + if (!isQueueProcessorRunning || (queueProcessorThread != null && !queueProcessorThread.isAlive())) { + isQueueProcessorRunning = false; + startCommandQueueProcessor(); + } + Activator.getLspProvider().getAmazonQServer().thenAcceptAsync(amazonQLspServer -> { try { switch (command) { - case CHAT_SEND_PROMPT: - ChatRequestParams chatRequestParams = jsonHandler.convertObject(params, ChatRequestParams.class); - chatRequestParams.setContext(chatRequestParams.getPrompt().context()); - addEditorState(chatRequestParams, true); - sendEncryptedChatMessage(chatRequestParams.getTabId(), token -> { - String encryptedMessage = lspEncryptionManager.encrypt(chatRequestParams); - - EncryptedChatParams encryptedChatRequestParams = new EncryptedChatParams(encryptedMessage, - token); - - return chatMessageProvider.sendChatPrompt(chatRequestParams.getTabId(), - encryptedChatRequestParams); - }); - break; - case CHAT_QUICK_ACTION: - QuickActionParams quickActionParams = jsonHandler.convertObject(params, QuickActionParams.class); - sendEncryptedChatMessage(quickActionParams.getTabId(), token -> { - String encryptedMessage = lspEncryptionManager.encrypt(quickActionParams); - - EncryptedQuickActionParams encryptedQuickActionParams = new EncryptedQuickActionParams( - encryptedMessage, token); - - return chatMessageProvider.sendQuickAction(quickActionParams.getTabId(), - encryptedQuickActionParams); - }); - break; - case CHAT_READY: - chatMessageProvider.sendChatReady(); - break; - case CHAT_TAB_ADD: - GenericTabParams tabParamsForAdd = jsonHandler.convertObject(params, GenericTabParams.class); - chatMessageProvider.sendTabAdd(tabParamsForAdd); - break; - case CHAT_TAB_REMOVE: - GenericTabParams tabParamsForRemove = jsonHandler.convertObject(params, GenericTabParams.class); - chatMessageProvider.sendTabRemove(tabParamsForRemove); - break; - case CHAT_TAB_CHANGE: - GenericTabParams tabParamsForChange = jsonHandler.convertObject(params, GenericTabParams.class); - chatMessageProvider.sendTabChange(tabParamsForChange); - break; - case CHAT_FOLLOW_UP_CLICK: - FollowUpClickParams followUpClickParams = jsonHandler.convertObject(params, - FollowUpClickParams.class); - chatMessageProvider.followUpClick(followUpClickParams); - break; - case CHAT_END_CHAT: - GenericTabParams tabParamsForEndChat = jsonHandler.convertObject(params, GenericTabParams.class); - chatMessageProvider.endChat(tabParamsForEndChat); - break; - case CHAT_FEEDBACK: - var feedbackParams = jsonHandler.convertObject(params, FeedbackParams.class); - chatMessageProvider.sendFeedback(feedbackParams); - break; - case TELEMETRY_EVENT: - chatMessageProvider.sendTelemetryEvent(params); - break; - default: - throw new AmazonQPluginException("Unexpected command received from Chat UI: " + command.toString()); + case CHAT_SEND_PROMPT: + message.addValueForKey("context", message.getValueForKey("prompt.context")); + addEditorState(message, true); + sendEncryptedChatMessage(message.getValueAsString("tabId"), token -> { + String encryptedMessage = lspEncryptionManager.encrypt(message.getData()); + EncryptedChatParams encryptedChatRequestParams = new EncryptedChatParams(encryptedMessage, + token); + String tabId = message.getValueAsString("tabId"); + var response = amazonQLspServer.sendChatPrompt(encryptedChatRequestParams); + inflightRequestByTabId.put(tabId, response); + return handleChatResponse(tabId, response); + }); + break; + case CHAT_PROMPT_OPTION_CHANGE: + amazonQLspServer.promptInputOptionChange(message.getData()); + break; + case CHAT_QUICK_ACTION: + sendEncryptedChatMessage(message.getValueAsString("tabId"), token -> { + String encryptedMessage = lspEncryptionManager.encrypt(message.getData()); + EncryptedQuickActionParams encryptedQuickActionParams = new EncryptedQuickActionParams( + encryptedMessage, token); + String tabId = message.getValueAsString("tabId"); + var response = amazonQLspServer.sendQuickAction(encryptedQuickActionParams); + return handleChatResponse(tabId, response); + }); + break; + case CHAT_READY: + amazonQLspServer.chatReady(); + startCommandQueueProcessor(); + break; + case CHAT_TAB_ADD: + amazonQLspServer.tabAdd(message.getData()); + break; + case CHAT_TAB_REMOVE: + lastProcessedTimeMap.remove(message.getValueAsString("tabId")); + amazonQLspServer.tabRemove(message.getData()); + break; + case CHAT_TAB_CHANGE: + amazonQLspServer.tabChange(message.getData()); + break; + case FILE_CLICK: + if (validateFileInWorkspaceRoot(message.getValueAsString("fullPath"))) { + amazonQLspServer.fileClick(message.getData()); + } + break; + case CHAT_INFO_LINK_CLICK: + amazonQLspServer.infoLinkClick(message.getData()); + break; + case CHAT_LINK_CLICK: + amazonQLspServer.linkClick(message.getData()); + break; + case CHAT_SOURCE_LINK_CLICK: + amazonQLspServer.sourceLinkClick(message.getData()); + break; + case CHAT_FOLLOW_UP_CLICK: + amazonQLspServer.followUpClick(message.getData()); + break; + case CHAT_END_CHAT: + amazonQLspServer.endChat(message.getData()); + break; + case CHAT_INSERT_TO_CURSOR_POSITION: + amazonQLspServer.sendTelemetryEvent(message.getData()); + break; + case CHAT_FEEDBACK: + amazonQLspServer.sendFeedback(message.getData()); + break; + case STOP_CHAT_RESPONSE: + cancelInflightRequests(message.getValueAsString("tabId")); + break; + case TELEMETRY_EVENT: + amazonQLspServer.sendTelemetryEvent(message.getData()); + break; + case LIST_CONVERSATIONS: + try { + Object response = amazonQLspServer.listConversations(message.getData()).get(); + var listConversationsCommand = ChatUIInboundCommand.createCommand("aws/chat/listConversations", + response); + Activator.getEventBroker().post(ChatUIInboundCommand.class, listConversationsCommand); + } catch (Exception e) { + Activator.getLogger().error("Error processing listConversations: " + e); + } + break; + case CONVERSATION_CLICK: + try { + Object response = amazonQLspServer.conversationClick(message.getData()).get(); + var conversationClickCommand = ChatUIInboundCommand.createCommand("aws/chat/conversationClick", + response); + Activator.getEventBroker().post(ChatUIInboundCommand.class, conversationClickCommand); + } catch (Exception e) { + Activator.getLogger().error("Error processing conversationClick: " + e); + } + break; + case CREATE_PROMPT: + amazonQLspServer.createPrompt(message.getData()); + break; + case TAB_BAR_ACTION: + try { + Object response = amazonQLspServer.tabBarAction(message.getData()).get(); + var tabBarActionsCommand = ChatUIInboundCommand.createCommand("aws/chat/tabBarAction", + response); + Activator.getEventBroker().post(ChatUIInboundCommand.class, tabBarActionsCommand); + } catch (Exception e) { + Activator.getLogger().error("Error processing tabBarActions: " + e); + } + break; + case BUTTON_CLICK: + String tabId = message.getValueAsString("tabId"); + ButtonClickResult response = amazonQLspServer.buttonClick(message.getData()).get(); + + if (!response.success()) { + sendErrorToUi(tabId, new Throwable(response.failureReason())); + } + break; + default: + throw new AmazonQPluginException("Unexpected command received from Chat UI: " + command.toString()); } } catch (Exception e) { throw new AmazonQPluginException("Error occurred when sending message to server", e); } - }, ThreadingUtils.getWorkerPool()); + }, ThreadingUtils.getWorkerPool()).exceptionally(throwable -> { + Activator.getLogger().error("Failed to process message: " + throwable.getMessage()); + return null; + }); } - public void sendInlineChatMessageToChatServer(final Object params) { - chatMessageProvider.thenAcceptAsync(chatMessageProvider -> { + public void sendInlineChatMessageToChatServer(final ChatMessage chatMessage) { + Activator.getLspProvider().getAmazonQServer().thenAcceptAsync(amazonQLspServer -> { try { - InlineChatRequestParams chatRequestParams = jsonHandler.convertObject(params, InlineChatRequestParams.class); - addEditorState(chatRequestParams, false); + addEditorState(chatMessage, false); sendEncryptedChatMessage(inlineChatTabId, token -> { - String encryptedMessage = lspEncryptionManager.encrypt(chatRequestParams); + String encryptedMessage = lspEncryptionManager.encrypt(chatMessage.getData()); EncryptedChatParams encryptedChatRequestParams = new EncryptedChatParams(encryptedMessage, token); - return chatMessageProvider.sendInlineChatPrompt(encryptedChatRequestParams); + return amazonQLspServer.sendInlineChatPrompt(encryptedChatRequestParams); }); } catch (Exception e) { throw new AmazonQPluginException("Error occurred when sending message to server", e); @@ -170,15 +261,10 @@ public void sendInlineChatMessageToChatServer(final Object params) { }); } - private BaseChatRequestParams addEditorState(final BaseChatRequestParams chatRequestParams, final boolean addCursorState) { - // only include files that are accessible via lsp which have absolute paths - getOpenFileUri().ifPresent(filePathUri -> { - chatRequestParams.setTextDocument(new TextDocumentIdentifier(filePathUri)); - if (addCursorState) { - getSelectionRangeCursorState().ifPresent(cursorState -> chatRequestParams.setCursorState(Arrays.asList(cursorState))); - } + private CompletableFuture handleChatResponse(final String tabId, final CompletableFuture response) { + return response.whenComplete((result, exception) -> { + inflightRequestByTabId.remove(tabId); }); - return chatRequestParams; } protected Optional getOpenFileUri() { @@ -192,6 +278,14 @@ public void run() { return fileUri.get(); } + public void cancelInflightRequests(final String tabId) { + var inflightRequest = inflightRequestByTabId.getOrDefault(tabId, null); + if (inflightRequest != null) { + inflightRequest.cancel(true); + inflightRequestByTabId.remove(tabId); + } + } + protected Optional getSelectionRangeCursorState() { AtomicReference> range = new AtomicReference>(); Display.getDefault().syncExec(new Runnable() { @@ -204,59 +298,144 @@ public void run() { return range.get().map(CursorState::new); } - private CompletableFuture sendEncryptedChatMessage(final String tabId, + private boolean validateFileInWorkspaceRoot(final String fullPath) { + if (fullPath == null) { + return true; + } + + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + IPath path = new Path(fullPath); + + try { + IProject[] projects = root.getProjects(); + boolean isInProjectRoot = false; + + for (IProject project : projects) { + if (project.isOpen()) { + IPath projectPath = project.getLocation(); + + if (projectPath.isPrefixOf(path)) { + isInProjectRoot = true; + break; + } + } + } + + return isInProjectRoot; + } catch (Exception e) { + Activator.getLogger().error("Error checking project paths", e); + } + + return false; + } + + private ChatMessage addEditorState(final ChatMessage chatRequestParams, final boolean addCursorState) { + // only include files that are accessible via lsp which have absolute paths + getOpenFileUri().ifPresent(filePathUri -> { + chatRequestParams.addValueForKey("textDocument", new TextDocumentIdentifier(filePathUri)); + if (addCursorState) { + getSelectionRangeCursorState().ifPresent( + cursorState -> chatRequestParams.addValueForKey("cursorState", Arrays.asList(cursorState))); + } + }); + return chatRequestParams; + } + + private CompletableFuture sendEncryptedChatMessage(final String tabId, final Function> action) { - // Retrieving the chat result is expected to be a long-running process with - // intermittent progress notifications being sent - // from the LSP server. The progress notifications provide a token and a partial - // result Object - we are utilizing a token to - // ChatMessage mapping to acquire the associated ChatMessage so we can formulate - // a message for the UI. String partialResultToken = addPartialChatMessage(tabId); + registerPartialResultToken(partialResultToken); return action.apply(partialResultToken).handle((encryptedChatResult, exception) -> { - // The mapping entry no longer needs to be maintained once the final result is - // retrieved. - removePartialChatMessage(partialResultToken); - if (exception != null) { - Activator.getLogger().error("An error occurred while processing chat request: " + exception.getMessage()); + // handle cancellations + if (exception instanceof CancellationException + || exception.getCause() instanceof CancellationException) { + ChatAsyncResultManager manager = ChatAsyncResultManager.getInstance(); + try { + manager.createRequestId(partialResultToken); + manager.getResult(partialResultToken); + handleCancellation(tabId); + } catch (Exception e) { + Activator.getLogger().error("An error occurred while processing cancellation: " + exception.getMessage()); + } finally { + manager.removeRequestId(partialResultToken); + partialResultLocks.remove(partialResultToken); + finalResultProcessed.remove(partialResultToken); + lastProcessedTimeMap.remove(tabId); + } + return null; + } + + // handle non-cancellation errors + Activator.getLogger() + .error("An error occurred while processing chat request: " + exception.getMessage()); sendErrorToUi(tabId, exception); + removePartialChatMessage(partialResultToken); + partialResultLocks.remove(partialResultToken); + finalResultProcessed.remove(partialResultToken); + lastProcessedTimeMap.remove(tabId); return null; - } else { - try { - String serializedData = lspEncryptionManager.decrypt(encryptedChatResult); - ChatResult result = jsonHandler.deserialize(serializedData, ChatResult.class); + } - if (result.codeReference() != null && result.codeReference().length >= 1) { - ChatCodeReference chatCodeReference = new ChatCodeReference(result.codeReference()); - Activator.getCodeReferenceLoggingService().log(chatCodeReference); + // process successful responses + removePartialChatMessage(partialResultToken); + try { + finalResultProcessed.put(partialResultToken, true); + String serializedData = lspEncryptionManager.decrypt(encryptedChatResult); + Map result = jsonHandler.deserialize(serializedData, Map.class); + + if (result.containsKey("codeReference")) { + ReferenceTrackerInformation[] codeReferences = ObjectMapperFactory.getInstance() + .convertValue(result.get("codeReference"), ReferenceTrackerInformation[].class); + if (codeReferences != null && codeReferences.length >= 1) { + Activator.getCodeReferenceLoggingService() + .log(new ChatCodeReference(codeReferences)); } + } - // show chat response in Chat UI - String command = (inlineChatTabId.equals(tabId)) + String command = inlineChatTabId.equals(tabId) ? ChatUIInboundCommandName.InlineChatPrompt.getValue() : ChatUIInboundCommandName.ChatPrompt.getValue(); - ChatUIInboundCommand chatUIInboundCommand = new ChatUIInboundCommand( - command, tabId, result, false); - sendMessageToChatUI(chatUIInboundCommand); - return result; - } catch (Exception e) { - Activator.getLogger().error("An error occurred while processing chat response received: " + e.getMessage()); - sendErrorToUi(tabId, e); - return null; - } + + sendMessageToChatUI(new ChatUIInboundCommand(command, tabId, result, false, null)); + return result; + } catch (Exception e) { + Activator.getLogger() + .error("An error occurred while processing chat response: " + e.getMessage()); + sendErrorToUi(tabId, e); + partialResultLocks.remove(partialResultToken); + finalResultProcessed.remove(partialResultToken); + return null; } }); } + void registerPartialResultToken(final String partialResultToken) { + Object lock = new Object(); + partialResultLocks.put(partialResultToken, lock); + finalResultProcessed.put(partialResultToken, false); + } + + // Workaround to properly report cancellation event to chatUI + private CompletableFuture handleCancellation(final String tabId) { + Activator.getLogger().info("Chat request was cancelled for tab: " + tabId); + lastProcessedTimeMap.remove(tabId); + + var errorParams = new ErrorParams(tabId, null, "", ""); + ChatUIInboundCommand inbound = new ChatUIInboundCommand( + ChatUIInboundCommandName.ErrorMessage.getValue(), tabId, errorParams, false, null); + sendMessageToChatUI(inbound); + return CompletableFuture.completedFuture(null); + } + private void sendErrorToUi(final String tabId, final Throwable exception) { String errorTitle = "An error occurred while processing your request."; String errorMessage = String.format("Details: %s", exception.getMessage()); ErrorParams errorParams = new ErrorParams(tabId, null, errorMessage, errorTitle); // show error in Chat UI ChatUIInboundCommand chatUIInboundCommand = new ChatUIInboundCommand( - ChatUIInboundCommandName.ErrorMessage.getValue(), tabId, errorParams, false); + ChatUIInboundCommandName.ErrorMessage.getValue(), tabId, errorParams, false, null); sendMessageToChatUI(chatUIInboundCommand); } @@ -280,23 +459,8 @@ public void removeListener(final ChatUiRequestListener listener) { } } - /* - * Sends message to Chat UI to show in webview - */ - public void sendMessageToChatUI(final ChatUIInboundCommand command) { - String message = jsonHandler.serialize(command); - String inlineChatCommand = ChatUIInboundCommandName.InlineChatPrompt.getValue(); - if (inlineChatCommand.equals(command.command())) { - inlineChatListenerFuture.thenApply(listener -> { - listener.onSendToChatUi(message); - return listener; - }); - } else { - chatUiRequestListenerFuture.thenApply(listener -> { - listener.onSendToChatUi(message); - return listener; - }); - } + public void activate() { + this.isActive = true; } /* @@ -313,7 +477,6 @@ public void handlePartialResultProgressNotification(final ProgressParams params) return; } - // Check to ensure Object is sent in params if (params.getValue().isLeft() || Objects.isNull(params.getValue().getRight())) { throw new AmazonQPluginException( "Error handling partial result notification: expected value of type Object"); @@ -321,22 +484,104 @@ public void handlePartialResultProgressNotification(final ProgressParams params) String encryptedPartialChatResult = ProgressNotificationUtils.getObject(params, String.class); String serializedData = lspEncryptionManager.decrypt(encryptedPartialChatResult); - ChatResult partialChatResult = jsonHandler.deserialize(serializedData, ChatResult.class); + Map partialChatResult = jsonHandler.deserialize(serializedData, Map.class); - // Check to ensure the body has content in order to keep displaying the spinner - // while loading - if (partialChatResult.body() == null || partialChatResult.body().length() == 0) { + if (partialChatResult == null) { return; } - String command = (inlineChatTabId.equals(tabId)) - ? ChatUIInboundCommandName.InlineChatPrompt.getValue() - : ChatUIInboundCommandName.ChatPrompt.getValue(); + String command = inlineChatTabId.equals(tabId) + ? ChatUIInboundCommandName.InlineChatPrompt.getValue() + : ChatUIInboundCommandName.ChatPrompt.getValue(); + + // special case: check for stop message before acquiring lock + @SuppressWarnings("unchecked") + List> additionalMessages = (List>) partialChatResult.get("additionalMessages"); + if (additionalMessages != null) { + for (Map message : additionalMessages) { + String messageId = (String) message.get("messageId"); + if (messageId != null && messageId.startsWith("stopped")) { + // process stop messages immediately + sendMessageToChatUI(new ChatUIInboundCommand(command, tabId, partialChatResult, true, null)); + finalResultProcessed.put(token, true); + ChatAsyncResultManager.getInstance().setResult(token, partialChatResult); + return; + } + } + } - ChatUIInboundCommand chatUIInboundCommand = new ChatUIInboundCommand( - command, tabId, partialChatResult, true); + // normal partial processing + Object lock = partialResultLocks.get(token); + if (lock == null) { + return; + } - sendMessageToChatUI(chatUIInboundCommand); + synchronized (lock) { + if (partialResultLocks.get(token) == null || Boolean.TRUE.equals(finalResultProcessed.get(token))) { + return; + } + + Object body = partialChatResult.get("body"); + boolean hasAdditionalMessages = (additionalMessages != null && !additionalMessages.isEmpty()); + long currentTime = System.currentTimeMillis(); + + // rate limit by discarding messages that have arrived too soon since the last was fired + if (!hasAdditionalMessages && body instanceof String) { + Long lastProcessedTime = lastProcessedTimeMap.get(tabId); + if (lastProcessedTime != null) { + int currentDelay = calculateDelay((String) body); + if ((currentTime - lastProcessedTime) < currentDelay) { + return; + } + } + } + + boolean insufficientContent = (body == null + || (body instanceof String && ((String) body).length() < MINIMUM_PARTIAL_RESPONSE_LENGTH)); + if (insufficientContent && !hasAdditionalMessages) { + return; + } + + // send partial response to UI if not cancelled in the interim + if (Boolean.FALSE.equals(finalResultProcessed.get(token))) { + sendMessageToChatUI(new ChatUIInboundCommand(command, tabId, partialChatResult, true, null)); + lastProcessedTimeMap.put(tabId, currentTime); + } + } + } + + private int calculateDelay(final String bodyString) { + if (bodyString == null || bodyString.isEmpty()) { + return MIN_DELAY_BETWEEN_PARTIALS; + } + int length = bodyString.length(); + double ratio = Math.min(1.0, (double) length / CHAR_COUNT_FOR_MAX_DELAY); + int delay = (int) (MIN_DELAY_BETWEEN_PARTIALS + (MAX_DELAY_BETWEEN_PARTIALS - MIN_DELAY_BETWEEN_PARTIALS) * ratio); + return delay; + } + + @Override + public void onEvent(final ChatUIInboundCommand command) { + commandQueue.add(command); + } + + /* + * Sends message to Chat UI to show in webview + */ + private void sendMessageToChatUI(final ChatUIInboundCommand command) { + String message = jsonHandler.serialize(command); + String inlineChatCommand = ChatUIInboundCommandName.InlineChatPrompt.getValue(); + if (inlineChatCommand.equals(command.command())) { + inlineChatListenerFuture.thenApply(listener -> { + listener.onSendToChatUi(message); + return listener; + }); + } else { + chatUiRequestListenerFuture.thenApply(listener -> { + listener.onSendToChatUi(message); + return listener; + }); + } } /* @@ -360,13 +605,50 @@ private String addPartialChatMessage(final String tabId) { * Removes an entry from the partialResultToken to ChatMessage's tabId map. */ private void removePartialChatMessage(final String partialResultToken) { + String tabId = chatPartialResultMap.getValue(partialResultToken); chatPartialResultMap.removeEntry(partialResultToken); + if (tabId != null) { + lastProcessedTimeMap.remove(tabId); + } + } + + private void startCommandQueueProcessor() { + if (isQueueProcessorRunning) { + return; + } + isQueueProcessorRunning = true; + ThreadingUtils.executeAsyncTask(() -> { + queueProcessorThread = Thread.currentThread(); + while (isQueueProcessorRunning && !Thread.currentThread().isInterrupted()) { + try { + if (!isActive) { + break; + } + ChatUIInboundCommand command = commandQueue.take(); + sendMessageToChatUI(command); + while ((command = commandQueue.poll()) != null) { + sendMessageToChatUI(command); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + isQueueProcessorRunning = false; + } catch (Exception e) { + Activator.getLogger().error("Error processing command from queue", e); + try { + Thread.sleep(100); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + isQueueProcessorRunning = false; + } + } + } + isQueueProcessorRunning = false; + }); } public static final class Builder { private JsonHandler jsonHandler; - private CompletableFuture chatMessageProvider; private ChatPartialResultMap chatPartialResultMap; private LspEncryptionManager lspEncryptionManager; @@ -375,11 +657,6 @@ public Builder withJsonHandler(final JsonHandler jsonHandler) { return this; } - public Builder withChatMessageProvider(final CompletableFuture chatMessageProvider) { - this.chatMessageProvider = chatMessageProvider; - return this; - } - public Builder withChatPartialResultMap(final ChatPartialResultMap chatPartialResultMap) { this.chatPartialResultMap = chatPartialResultMap; return this; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessage.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessage.java index 1779ef26a..3320d458e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessage.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessage.java @@ -3,65 +3,38 @@ package software.aws.toolkits.eclipse.amazonq.chat; -import java.util.concurrent.CompletableFuture; +import com.fasterxml.jackson.databind.JsonNode; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; -import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; +import software.aws.toolkits.eclipse.amazonq.util.JsonHandler; public final class ChatMessage { - private final AmazonQLspServer amazonQLspServer; + private final JsonHandler jsonHandler; + private Object data; - public ChatMessage(final AmazonQLspServer amazonQLspServer) { - this.amazonQLspServer = amazonQLspServer; + public ChatMessage(final Object data) { + this.jsonHandler = new JsonHandler(); + this.data = data; } - // Returns a ChatResult as an encrypted message {@link LspEncryptionManager#decrypt()} - public CompletableFuture sendChatPrompt(final EncryptedChatParams params) { - return amazonQLspServer.sendChatPrompt(params); + public boolean hasKey(final String key) { + return jsonHandler.getValueForKey(data, key) != null; } - public CompletableFuture sendInlineChatPrompt(final EncryptedChatParams params) { - return amazonQLspServer.sendInlineChatPrompt(params); + public JsonNode getValueForKey(final String key) { + return jsonHandler.getValueForKey(data, key); } - // Returns a ChatResult as an encrypted message {@link LspEncryptionManager#decrypt()} - public CompletableFuture sendQuickAction(final EncryptedQuickActionParams params) { - return amazonQLspServer.sendQuickAction(params); + public void addValueForKey(final String key, final Object obj) { + data = jsonHandler.addValueForKey(data, key, obj); } - public CompletableFuture endChat(final GenericTabParams tabParams) { - return amazonQLspServer.endChat(tabParams); + public Object getData() { + return data; } - public void sendChatReady() { - amazonQLspServer.chatReady(); + public String getValueAsString(final String key) { + JsonNode node = jsonHandler.getValueForKey(data, key); + return node != null ? node.asText() : null; } - public void sendTabAdd(final GenericTabParams tabParams) { - amazonQLspServer.tabAdd(tabParams); - } - - public void sendTabRemove(final GenericTabParams tabParams) { - amazonQLspServer.tabRemove(tabParams); - } - - public void sendTabChange(final GenericTabParams tabParams) { - amazonQLspServer.tabChange(tabParams); - } - - public void followUpClick(final FollowUpClickParams followUpClickParams) { - amazonQLspServer.followUpClick(followUpClickParams); - } - - public void sendFeedback(final FeedbackParams feedbackParams) { - amazonQLspServer.sendFeedback(feedbackParams); - } - - public void sendTelemetryEvent(final Object params) { - amazonQLspServer.sendTelemetryEvent(params); - } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageProvider.java deleted file mode 100644 index 691cb0493..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageProvider.java +++ /dev/null @@ -1,115 +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.chat; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; -import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; - -public final class ChatMessageProvider { - - private final AmazonQLspServer amazonQLspServer; - // Map of in-flight requests per tab Ids - // TODO ECLIPSE-349: Handle disposing resources of this class including this map - private Map> inflightRequestByTabId = new ConcurrentHashMap>(); - - public static CompletableFuture createAsync() { - return Activator.getLspProvider().getAmazonQServer() - .thenApply(ChatMessageProvider::new); - } - - private ChatMessageProvider(final AmazonQLspServer amazonQLspServer) { - this.amazonQLspServer = amazonQLspServer; - } - - public CompletableFuture sendChatPrompt(final String tabId, final EncryptedChatParams encryptedChatRequestParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - - var response = chatMessage.sendChatPrompt(encryptedChatRequestParams); - // We assume there is only one outgoing request per tab because the input is - // blocked when there is an outgoing request - inflightRequestByTabId.put(tabId, response); - - return handleChatResponse(tabId, response); - } - - public CompletableFuture sendInlineChatPrompt(final EncryptedChatParams encryptedChatRequestParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - return chatMessage.sendInlineChatPrompt(encryptedChatRequestParams); - } - - public CompletableFuture sendQuickAction(final String tabId, final EncryptedQuickActionParams encryptedQuickActionParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - - var response = chatMessage.sendQuickAction(encryptedQuickActionParams); - // We assume there is only one outgoing request per tab because the input is - // blocked when there is an outgoing request - inflightRequestByTabId.put(tabId, response); - - return handleChatResponse(tabId, response); - } - - private CompletableFuture handleChatResponse(final String tabId, final CompletableFuture response) { - return response.whenComplete((result, exception) -> { - inflightRequestByTabId.remove(tabId); - }); - } - - public CompletableFuture endChat(final GenericTabParams tabParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - return chatMessage.endChat(tabParams); - } - - public void sendChatReady() { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.sendChatReady(); - } - - public void sendTabAdd(final GenericTabParams tabParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.sendTabAdd(tabParams); - } - - public void sendTabRemove(final GenericTabParams tabParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - cancelInflightRequests(tabParams.tabId()); - chatMessage.sendTabRemove(tabParams); - } - - public void sendTabChange(final GenericTabParams tabParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.sendTabChange(tabParams); - } - - public void followUpClick(final FollowUpClickParams followUpClickParams) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.followUpClick(followUpClickParams); - } - - public void sendTelemetryEvent(final Object params) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.sendTelemetryEvent(params); - } - - public void sendFeedback(final FeedbackParams params) { - ChatMessage chatMessage = new ChatMessage(amazonQLspServer); - chatMessage.sendFeedback(params); - } - - private void cancelInflightRequests(final String tabId) { - var inflightRequest = inflightRequestByTabId.getOrDefault(tabId, null); - if (inflightRequest != null) { - inflightRequest.cancel(true); - inflightRequestByTabId.remove(tabId); - } - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatPartialResultMap.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatPartialResultMap.java index 661caeebb..f1a0bf01a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatPartialResultMap.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatPartialResultMap.java @@ -48,7 +48,4 @@ public String getValue(final String token) { return tokenToChatMessageMap.getOrDefault(token, null); } - public Boolean hasKey(final String token) { - return tokenToChatMessageMap.containsKey(token); - } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java deleted file mode 100644 index fa87c3c7a..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatStateManager.java +++ /dev/null @@ -1,75 +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.chat; - -import org.eclipse.swt.SWT; -import org.eclipse.swt.browser.Browser; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.PlatformUI; - -public final class ChatStateManager { - private static ChatStateManager instance; - private Browser browser; - private Composite dummyParent; - private volatile boolean hasPreservedState = false; - - public static synchronized ChatStateManager getInstance() { - if (instance == null) { - instance = new ChatStateManager(); - } - return instance; - } - - public synchronized Browser getBrowser(final Composite parent) { - // if browser is null or disposed, return null - if (browser == null || browser.isDisposed()) { - return null; - } else if (browser.getParent() != parent) { - // Re-parent existing browser - browser.setParent(parent); - disposeDummyParent(); - } - return browser; - } - - public synchronized void updateBrowser(final Browser browser) { - // resetting browser indicates that no state is preserved - hasPreservedState = false; - this.browser = browser; - } - - public synchronized boolean hasPreservedState() { - return hasPreservedState; - } - - public void preserveBrowser() { - if (browser != null && !browser.isDisposed()) { - if (dummyParent == null || dummyParent.isDisposed()) { - dummyParent = new Composite( - PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), - SWT.NONE - ); - dummyParent.setVisible(false); - } - browser.setParent(dummyParent); - hasPreservedState = true; - } - } - - private void disposeDummyParent() { - if (dummyParent != null && !dummyParent.isDisposed()) { - dummyParent.dispose(); - dummyParent = null; - } - } - - public void dispose() { - if (browser != null && !browser.isDisposed()) { - browser.dispose(); - browser = null; - } - disposeDummyParent(); - hasPreservedState = false; - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java index 6308426dd..717e0af4e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatTheme.java @@ -6,54 +6,22 @@ import java.util.HashMap; import java.util.Map; -import org.eclipse.swt.browser.Browser; - import software.aws.toolkits.eclipse.amazonq.chat.models.QChatCssVariable; import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; public final class ChatTheme { - private static final String CHAT_THEME_STYLE_TITLE = "CHAT_THEME_STYLE"; - - private ThemeDetector themeDetector; + private ThemeDetector themeDetector; public ChatTheme() { this.themeDetector = new ThemeDetector(); } - public void injectTheme(final Browser browser) { - String css = ""; - - if (themeDetector.isDarkTheme()) { - css = getCssForDarkTheme(); - } else { - css = getCssForLightTheme(); - } - - String removeExistingThemeScript = String.format(""" - var sheets = document.styleSheets;\ - for (var i=0; i themeMap = themeDetector.isDarkTheme() ? getDarkThemeMap() : getLightThemeMap(); + return getCss(themeMap); } - private String getCssForDarkTheme() { + private Map getDarkThemeMap() { Map themeMap = new HashMap<>(); String defaultTextColor = rgb(238, 238, 238); @@ -65,10 +33,11 @@ private String getCssForDarkTheme() { themeMap.put(QChatCssVariable.TextColorWeak, rgba(205, 205, 205, 0.5)); themeMap.put(QChatCssVariable.TextColorLink, rgb(102, 168, 245)); themeMap.put(QChatCssVariable.TextColorInput, defaultTextColor); + themeMap.put(QChatCssVariable.TextColorAlternate, defaultTextColor); // Layout themeMap.put(QChatCssVariable.Background, rgb(47, 47, 47)); - themeMap.put(QChatCssVariable.TabActive, cardBackgroundColor); + themeMap.put(QChatCssVariable.TabActive, rgb(51, 51, 51)); themeMap.put(QChatCssVariable.BorderDefault, rgb(76, 76, 76)); themeMap.put(QChatCssVariable.ColorToggle, rgb(30, 30, 30)); @@ -90,7 +59,7 @@ private String getCssForDarkTheme() { themeMap.put(QChatCssVariable.StatusError, rgb(255, 102, 102)); // Buttons - themeMap.put(QChatCssVariable.ButtonBackground, rgb(51, 118, 205)); + themeMap.put(QChatCssVariable.ButtonBackground, rgb(14, 99, 156)); themeMap.put(QChatCssVariable.ButtonForeground, rgb(255, 255, 255)); // Alternates @@ -99,13 +68,19 @@ private String getCssForDarkTheme() { // Card themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor); + themeMap.put(QChatCssVariable.CardBackgroundAlternate, rgb(31, 31, 34)); themeMap.put(QChatCssVariable.LineHeight, "1.25em"); - return getCss(themeMap); + // Input + themeMap.put(QChatCssVariable.InputBackground, rgb(60, 60, 60)); + themeMap.put(QChatCssVariable.InputBorder, rgb(76, 76, 76)); + themeMap.put(QChatCssVariable.InputBorderFocused, rgb(14, 99, 156)); + + return themeMap; } - private String getCssForLightTheme() { + private Map getLightThemeMap() { Map themeMap = new HashMap<>(); String defaultTextColor = rgb(10, 10, 10); @@ -117,10 +92,11 @@ private String getCssForLightTheme() { themeMap.put(QChatCssVariable.TextColorWeak, rgba(45, 45, 45, 0.5)); themeMap.put(QChatCssVariable.TextColorLink, rgb(59, 34, 246)); themeMap.put(QChatCssVariable.TextColorInput, defaultTextColor); + themeMap.put(QChatCssVariable.TextColorAlternate, defaultTextColor); // Layout - themeMap.put(QChatCssVariable.Background, rgb(250, 250, 250)); - themeMap.put(QChatCssVariable.TabActive, cardBackgroundColor); + themeMap.put(QChatCssVariable.Background, rgb(243, 243, 243)); + themeMap.put(QChatCssVariable.TabActive, rgb(250, 250, 250)); themeMap.put(QChatCssVariable.BorderDefault, rgb(230, 230, 230)); themeMap.put(QChatCssVariable.ColorToggle, rgb(220, 220, 220)); @@ -151,13 +127,19 @@ private String getCssForLightTheme() { // Card themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor); + themeMap.put(QChatCssVariable.CardBackgroundAlternate, rgb(255, 255, 255)); themeMap.put(QChatCssVariable.LineHeight, "1.25em"); - return getCss(themeMap); + // Input + themeMap.put(QChatCssVariable.InputBackground, rgb(255, 255, 255)); + themeMap.put(QChatCssVariable.InputBorder, rgb(230, 230, 230)); + themeMap.put(QChatCssVariable.InputBorderFocused, rgb(51, 118, 205)); + + return themeMap; } - private String getCss(final Map themeMap) { + private String getCss(final Map themeMap) { StringBuilder variables = new StringBuilder(); for (var entry : themeMap.entrySet()) { @@ -165,7 +147,7 @@ private String getCss(final Map themeMap) { continue; } - variables.append(String.format("%s:%s;", + variables.append(String.format("%s:%s !important;", entry.getKey().getValue(), entry.getValue())); } @@ -173,12 +155,11 @@ private String getCss(final Map themeMap) { return String.format(":root{%s}", variables.toString()); } - private String rgb(final Integer r, final Integer g, final Integer b) { + private String rgb(final Integer r, final Integer g, final Integer b) { return String.format("rgb(%s,%s,%s)", r, g, b); } - private String rgba(final Integer r, final Integer g, final Integer b, final Double a) { - return String.format("rgb(%s,%s,%s,%s)", r, g, b, a); + private String rgba(final Integer r, final Integer g, final Integer b, final Double a) { + return String.format("rgba(%s,%s,%s,%s)", r, g, b, a); } - } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/BaseChatRequestParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/BaseChatRequestParams.java index 81368ebe2..17c047728 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/BaseChatRequestParams.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/BaseChatRequestParams.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.chat.models; import java.util.List; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickParams.java new file mode 100644 index 000000000..5088b5a9e --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickParams.java @@ -0,0 +1,11 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ButtonClickParams(@JsonProperty("tabId") String tabId, + @JsonProperty("messageId") String messageId, + @JsonProperty("buttonId") String buttonId) { +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickResult.java new file mode 100644 index 000000000..00559d535 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ButtonClickResult.java @@ -0,0 +1,10 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ButtonClickResult(@JsonProperty("success") Boolean success, + @JsonProperty("failureReason") String failureReason) { +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPrompt.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPrompt.java index 25688e049..057e768b7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPrompt.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPrompt.java @@ -7,11 +7,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import software.aws.toolkits.eclipse.amazonq.lsp.model.Command; - public record ChatPrompt( @JsonProperty("prompt") String prompt, @JsonProperty("escapedPrompt") String escapedPrompt, @JsonProperty("command") String command, - @JsonProperty("context") List context + @JsonProperty("context") List context ) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatRequestParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatRequestParams.java index 40c8e12f2..f5d58f698 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatRequestParams.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatRequestParams.java @@ -9,18 +9,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import software.aws.toolkits.eclipse.amazonq.lsp.model.Command; - public final class ChatRequestParams extends BaseChatRequestParams { private final String tabId; - private List context; + private List context; public ChatRequestParams( @JsonProperty("tabId") final String tabId, @JsonProperty("prompt") final ChatPrompt prompt, @JsonProperty("textDocument") final TextDocumentIdentifier textDocument, @JsonProperty("cursorState") final List cursorState, - @JsonProperty("context") final List context + @JsonProperty("context") final List context ) { super(prompt, textDocument, cursorState); this.tabId = tabId; @@ -31,11 +29,11 @@ public String getTabId() { return tabId; } - public void setContext(final List context) { + public void setContext(final List context) { this.context = context; } - public List getContext() { + public List getContext() { return context; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResult.java deleted file mode 100644 index 3c9958218..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResult.java +++ /dev/null @@ -1,18 +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.chat.models; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -// Mynah-ui will not render the partial result if null values are included. Must ignore nulls values. -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ChatResult( - @JsonProperty("body") String body, - @JsonProperty("messageId") String messageId, - @JsonProperty("canBeVoted") Boolean canBeVoted, - @JsonProperty("relatedContent") RelatedContent relatedContent, - @JsonProperty("followUp") FollowUp followUp, - @JsonProperty("codeReference") ReferenceTrackerInformation[] codeReference -) { }; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatTriggerType.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatTriggerType.java deleted file mode 100644 index 2458d751e..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatTriggerType.java +++ /dev/null @@ -1,5 +0,0 @@ -package software.aws.toolkits.eclipse.amazonq.chat.models; - -public enum ChatTriggerType { - CHAT_PROMPT, INLINE_CHAT -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommand.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommand.java index 63f6a8465..575fe2b0d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommand.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommand.java @@ -3,16 +3,19 @@ package software.aws.toolkits.eclipse.amazonq.chat.models; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; /** * Represents a command that is being sent to Q Chat UI. */ +@JsonInclude(JsonInclude.Include.NON_NULL) public record ChatUIInboundCommand( @JsonProperty("command") String command, @JsonProperty("tabId") String tabId, @JsonProperty("params") Object params, - @JsonProperty("isPartialResult") Boolean isPartialResult + @JsonProperty("isPartialResult") Boolean isPartialResult, + @JsonProperty("requestId") String requestId ) { public static ChatUIInboundCommand createGenericCommand(final GenericCommandParams params) { @@ -20,6 +23,7 @@ public static ChatUIInboundCommand createGenericCommand(final GenericCommandPara ChatUIInboundCommandName.GenericCommand.getValue(), null, params, + null, null ); } @@ -29,7 +33,28 @@ public static ChatUIInboundCommand createSendToPromptCommand(final SendToPromptP ChatUIInboundCommandName.SendToPrompt.getValue(), null, params, + null, null ); } + + public static ChatUIInboundCommand createCommand(final String commandName, final Object params) { + return new ChatUIInboundCommand( + commandName, + null, + params, + null, + null + ); + } + + public static ChatUIInboundCommand createCommand(final String commandName, final Object params, final String requestId) { + return new ChatUIInboundCommand( + commandName, + null, + params, + null, + requestId + ); + } }; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandName.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandName.java index 7b962612f..a08b6ac1f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandName.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandName.java @@ -6,6 +6,7 @@ public enum ChatUIInboundCommandName { ChatPrompt("aws/chat/sendChatPrompt"), // This is the odd one out, it follows the same message name as the request. InlineChatPrompt("aws/chat/sendInlineChatPrompt"), + OpenTab("aws/chat/openTab"), SendToPrompt("sendToPrompt"), ErrorMessage("errorMessage"), diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/FileClickParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/FileClickParams.java new file mode 100644 index 000000000..dcec5097f --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/FileClickParams.java @@ -0,0 +1,68 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonInclude; + +public final class FileClickParams { + @JsonProperty("tabId") + private String tabId; + + @JsonProperty("filePath") + private String filePath; + + @JsonProperty("action") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String action; + + @JsonProperty("messageId") + private String messageId; + + @JsonProperty("fullPath") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String fullPath; + + // Getters and Setters + public String getTabId() { + return tabId; + } + + public void setTabId(final String tabId) { + this.tabId = tabId; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(final String filePath) { + this.filePath = filePath; + } + + public String getAction() { + return action; + } + + public void setAction(final String action) { + this.action = action; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(final String messageId) { + this.messageId = messageId; + } + + public String getFullPath() { + return fullPath; + } + + public void setFullPath(final String fullPath) { + this.fullPath = fullPath; + } +} + diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParams.java similarity index 69% rename from plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParams.java rename to plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParams.java index c8b1285df..04543795c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParams.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParams.java @@ -4,8 +4,10 @@ package software.aws.toolkits.eclipse.amazonq.chat.models; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonInclude; -public class InfoLinkClickParams { +@JsonInclude(JsonInclude.Include.NON_NULL) +public class GenericLinkClickParams { @JsonProperty("tabId") private String tabId; @@ -15,6 +17,9 @@ public class InfoLinkClickParams { @JsonProperty("eventId") private String eventId; + @JsonProperty("messageId") + private String messageId; + public final String getTabId() { return tabId; } @@ -38,4 +43,12 @@ public final String getEventId() { public final void setEventId(final String eventId) { this.eventId = eventId; } + + public final String getMessageId() { + return messageId; + } + + public final void setMessageId(final String messageId) { + this.messageId = messageId; + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatParams.java new file mode 100644 index 000000000..2878ad31d --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatParams.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +public record GetSerializedChatParams(String tabId, String format) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatResult.java new file mode 100644 index 000000000..4e5de9618 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/GetSerializedChatResult.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +public record GetSerializedChatResult(boolean success, SerializedChatResult result) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/PromptInputOptionChangeParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/PromptInputOptionChangeParams.java new file mode 100644 index 000000000..aa2e3fa32 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/PromptInputOptionChangeParams.java @@ -0,0 +1,16 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PromptInputOptionChangeParams( + @JsonProperty("tabId") String tabId, + @JsonProperty("optionsValues") Map optionValues, + @JsonProperty("eventId") String eventId) { +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java index 14bcd1b38..834f077a9 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariable.java @@ -11,6 +11,7 @@ public enum QChatCssVariable { TextColorWeak("--mynah-color-text-weak"), TextColorLink("--mynah-color-text-link"), TextColorInput("--mynah-color-text-input"), + TextColorAlternate("--mynah-color-text-alternate"), // Layout Background("--mynah-color-bg"), @@ -45,9 +46,17 @@ public enum QChatCssVariable { // Card CardBackground("--mynah-card-bg"), + CardBackgroundAlternate("--mynah-card-bg-alternate"), // Line height - LineHeight("--mynah-line-height"); + LineHeight("--mynah-line-height"), + + // Input + InputBackground("--mynah-input-bg"), + + // Borders + InputBorder("--mynah-color-text-input-border"), + InputBorderFocused("--mynah-color-text-input-border-focused"); private String value; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/SerializedChatResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/SerializedChatResult.java new file mode 100644 index 000000000..0df654881 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/SerializedChatResult.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +public record SerializedChatResult(String content) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogParams.java new file mode 100644 index 000000000..984716042 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogParams.java @@ -0,0 +1,8 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import java.util.List; + +public record ShowSaveFileDialogParams(List supportedFormats, String defaultUri) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogResult.java new file mode 100644 index 000000000..588a2d80b --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/models/ShowSaveFileDialogResult.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +public record ShowSaveFileDialogResult(String targetUri) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/DefaultPluginStore.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/DefaultPluginStore.java index 914cd9129..aa277efc9 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/DefaultPluginStore.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/DefaultPluginStore.java @@ -19,7 +19,7 @@ public final class DefaultPluginStore implements PluginStore { private static DefaultPluginStore instance; - private IEclipsePreferences preferences; + private volatile IEclipsePreferences preferences; public DefaultPluginStore(final IEclipsePreferences preferences) { this.preferences = preferences != null ? preferences : InstanceScope.INSTANCE.getNode("software.aws.toolkits.eclipse"); @@ -34,7 +34,7 @@ public static synchronized DefaultPluginStore getInstance() { } @Override - public void put(final String key, final String value) { + public synchronized void put(final String key, final String value) { preferences.put(key, value); try { preferences.flush(); @@ -49,7 +49,7 @@ public String get(final String key) { } @Override - public void remove(final String key) { + public synchronized void remove(final String key) { preferences.remove(key); try { preferences.flush(); @@ -64,7 +64,7 @@ public void addChangeListener(final IPreferenceChangeListener prefChangeListener } @Override - public void putObject(final String key, final T value) { + public synchronized void putObject(final String key, final T value) { String jsonValue = GSON.toJson(value); byte[] byteValue = jsonValue.getBytes(StandardCharsets.UTF_8); preferences.putByteArray(key, byteValue); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/PluginStoreKeys.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/PluginStoreKeys.java index 8088343cc..5787ad11c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/PluginStoreKeys.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/PluginStoreKeys.java @@ -10,5 +10,6 @@ private PluginStoreKeys() { } public static final String CHAT_DISCLAIMER_ACKNOWLEDGED = "qchatDisclaimerAcknowledged"; + public static final String PAIR_PROGRAMMING_ACKNOWLEDGED = "qchatPairProgrammingAcknowledged"; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/customization/CustomizationUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/customization/CustomizationUtil.java new file mode 100644 index 000000000..951b90eb9 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/customization/CustomizationUtil.java @@ -0,0 +1,90 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.configuration.customization; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; +import org.eclipse.swt.widgets.Display; + +import software.amazon.awssdk.utils.StringUtils; +import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams.ExpectedResponseType; +import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.util.Constants; +import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification; +import software.aws.toolkits.eclipse.amazonq.views.model.Customization; + +public final class CustomizationUtil { + + private CustomizationUtil() { + // to avoid initiation + } + + public static void triggerChangeConfigurationNotification() { + try { + Activator.getLogger().info("Triggering configuration pull from Amazon Q LSP server"); + Activator.getLspProvider().getAmazonQServer() + .thenAccept(server -> { + server.getWorkspaceService().didChangeConfiguration(new DidChangeConfigurationParams()); + }).get(); + } catch (Exception e) { + Activator.getLogger().error("Error occurred while sending change configuration notification to Amazon Q LSP server", e); + throw new AmazonQPluginException(e); + } + } + + public static CompletableFuture> listCustomizations() { + GetConfigurationFromServerParams params = new GetConfigurationFromServerParams( + ExpectedResponseType.CUSTOMIZATION); + return Activator.getLspProvider().getAmazonQServer() + .thenCompose(server -> { + CompletableFuture> config = server + .getConfigurationFromServer(params); + return config; + }) + .thenApply(configurations -> Optional.ofNullable(configurations) + .map(config -> config.getConfigurations().stream() + .filter(customization -> customization != null && StringUtils.isNotBlank(customization.getName())) + .collect(Collectors.toList())) + .orElse(Collections.emptyList())) + .exceptionally(throwable -> { + Activator.getLogger().error("Error occurred while fetching the list of customizations", throwable); + throw new AmazonQPluginException(throwable); + }); + } + + public static void validateCurrentCustomization() { + listCustomizations().thenAccept(customizations -> { + Customization currentCustomization = Activator.getPluginStore() + .getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class); + + for (final Customization validCustomization : customizations) { + if (validCustomization.getArn().equals(currentCustomization.getArn())) { + return; + } + } + + // Use default customization + Activator.getPluginStore().remove(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY); + Display.getDefault() + .asyncExec(() -> CustomizationUtil.showNotification(Constants.DEFAULT_Q_FOUNDATION_DISPLAY_NAME)); + }); + } + + public static void showNotification(final String customizationName) { + AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), + Constants.IDE_CUSTOMIZATION_NOTIFICATION_TITLE, + String.format(Constants.IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE, customizationName)); + notification.open(); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java new file mode 100644 index 000000000..83969b645 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java @@ -0,0 +1,365 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.configuration.profiles; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; +import org.eclipse.swt.widgets.Display; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.awssdk.utils.StringUtils; +import software.aws.toolkits.eclipse.amazonq.broker.events.QDeveloperProfileState; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthStateType; +import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams.ExpectedResponseType; +import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.util.Constants; +import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; +import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; +import software.aws.toolkits.eclipse.amazonq.util.ToolkitNotification; +import software.aws.toolkits.eclipse.amazonq.views.ViewConstants; +import software.aws.toolkits.eclipse.amazonq.views.model.Customization; +import software.aws.toolkits.eclipse.amazonq.views.model.QDeveloperProfile; +import software.aws.toolkits.eclipse.amazonq.views.model.UpdateConfigurationParams; + +public final class QDeveloperProfileUtil { + + private static final QDeveloperProfileUtil INSTANCE; + private QDeveloperProfile savedDeveloperProfile; + private QDeveloperProfile selectedDeveloperProfile; + private CompletableFuture profileSelectionTask; + private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getInstance(); + private List profiles; + private ReentrantLock profilesLock = new ReentrantLock(true); + + static { + INSTANCE = new QDeveloperProfileUtil(); + } + + public static QDeveloperProfileUtil getInstance() { + return INSTANCE; + } + + private QDeveloperProfileUtil() { // prevent initialization + try { + savedDeveloperProfile = Optional + .ofNullable(Activator.getPluginStore().get(ViewConstants.Q_DEVELOPER_PROFILE_SELECTION_KEY)) + .map(json -> { + try { + if (isValidSerializedProfile(json)) { + return deserializeProfile(json); + } else { + Activator.getLogger().error("Cached profile has invalid format"); + } + } catch (final JsonProcessingException e) { + Activator.getLogger().error("Failed to process cached profile", e); + } + return null; + }).orElse(null); + + } catch (Exception e) { + Activator.getLogger().error("Failed to deserialize developer profile", e); + } + profileSelectionTask = new CompletableFuture<>(); + profiles = new ArrayList<>(); + } + + private boolean isValidSerializedProfile(final String profile) throws JsonProcessingException { + JsonNode node = OBJECT_MAPPER.readTree(profile); + return node.has("arn") && isValidArn(node.get("arn").asText()) && node.has("name") + && StringUtils.isNotBlank(node.get("name").asText()) && node.has("accountId") + && isValidAccountId(node.get("accountId").asText()) && node.has("region") + && node.get("identityDetails").has("region") + && isValidRegion(node.get("identityDetails").get("region").asText()); + } + + private QDeveloperProfile deserializeProfile(final String json) throws JsonProcessingException { + QDeveloperProfile deserializedProfile = OBJECT_MAPPER.readValue(json, QDeveloperProfile.class); + + if (!isValidProfile(deserializedProfile)) { + throw new JsonProcessingException("Cached profile has invalid data") { + private static final long serialVersionUID = 1L; + }; + } + return deserializedProfile; + } + + private String serializeProfile(final QDeveloperProfile developerProfile) throws JsonProcessingException { + if (!isValidProfile(developerProfile)) { + throw new JsonProcessingException("Developer profile has invalid data") { + private static final long serialVersionUID = 1L; + }; + } + + return OBJECT_MAPPER.writeValueAsString(developerProfile); + } + + public void initialize() { + if (savedDeveloperProfile != null) { + selectedDeveloperProfile = savedDeveloperProfile; + queryForDeveloperProfilesFuture(true, true).exceptionally(throwable -> { + Activator.getLogger().error( + "Plugin initialization with saved developer profile failed. Prompting user to log back in."); + Activator.getEventBroker().post(AuthState.class, + new AuthState(AuthStateType.LOGGED_OUT, LoginType.IAM_IDENTITY_CENTER)); + return null; + }).thenAccept(result -> { + CustomizationUtil.validateCurrentCustomization(); + }); + savedDeveloperProfile = null; + } + } + + public synchronized CompletableFuture> queryForDeveloperProfilesFuture( + final boolean tryApplyCachedProfile) { + return queryForDeveloperProfilesFuture(tryApplyCachedProfile, false); + } + + private synchronized CompletableFuture> queryForDeveloperProfilesFuture( + final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) { + return Activator.getLspProvider().getAmazonQServer() + .thenCompose(server -> { + GetConfigurationFromServerParams params = new GetConfigurationFromServerParams( + ExpectedResponseType.Q_DEVELOPER_PROFILE); + CompletableFuture> response = server + .getConfigurationFromServer(params); + return response; + }).thenApply(this::processConfigurations).exceptionally(throwable -> { + Activator.getLogger().error("Error occurred while fetching the list of Q Developer Profile: ", + throwable); + throw new AmazonQPluginException(throwable); + }).thenApply(result -> { + return handleSelectedProfile(result, tryApplyCachedProfile, applyProfileUnconditionally); + }); + } + + public synchronized List queryForDeveloperProfiles(final boolean tryApplyCachedProfile) throws ExecutionException { + try { + return queryForDeveloperProfilesFuture(tryApplyCachedProfile, false).get(); + } catch (InterruptedException e) { + Activator.getLogger().error("Interrupted when fetching profile: ", e); + } + + return new ArrayList<>(); + } + + public synchronized CompletableFuture getProfileSelectionTaskFuture() { + if (profileSelectionTask != null && !profileSelectionTask.isDone()) { + return profileSelectionTask; + } + profileSelectionTask = new CompletableFuture(); + return profileSelectionTask; + } + + private boolean isValidProfile(final QDeveloperProfile profile) { + return profile != null && StringUtils.isNotBlank(profile.getName()) && isValidAccountId(profile.getAccountId()) + && isValidArn(profile.getArn()) && isValidRegion(profile.getRegion()); + } + + private boolean isValidAccountId(final String accountId) { + return accountId != null && accountId.matches("^\\d{12}$"); + } + + private boolean isValidArn(final String arn) { + return arn != null && arn.matches("^arn:aws:codewhisperer:[a-z]{2}-[a-z]+-\\d:\\d{12}:profile/[A-Z0-9]+$"); + } + + private boolean isValidRegion(final String region) { + return region != null && region + .matches("^[a-z]{2}-(central|north|south|east|west|northeast|southeast|northwest|southwest)-(\\d)$"); + } + + private List handleSelectedProfile(final List profiles, + final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) { + boolean isProfileSet = false; + if (profiles.size() <= 1) { + isProfileSet = handleSingleOrNoProfile(profiles, tryApplyCachedProfile, applyProfileUnconditionally); + } else { + isProfileSet = handleMultipleProfiles(profiles, tryApplyCachedProfile, applyProfileUnconditionally); + } + + if (!isProfileSet) { + setProfiles(profiles); + Activator.getEventBroker().post(QDeveloperProfileState.class, QDeveloperProfileState.AVAILABLE); + } + return profiles; + } + + private List getProfiles() { + try { + profilesLock.lock(); + return profiles; + } finally { + profilesLock.unlock(); + } + } + + private void setProfiles(final List profiles) { + try { + profilesLock.lock(); + this.profiles = profiles; + } finally { + profilesLock.unlock(); + } + } + + private boolean handleSingleOrNoProfile(final List profiles, + final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) { + if (!profiles.isEmpty() && tryApplyCachedProfile) { + setDeveloperProfile(profiles.get(0), true, applyProfileUnconditionally); + return true; + } + return false; + } + + private boolean handleMultipleProfiles(final List profiles, + final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) { + boolean isProfileSelected = false; + if (selectedDeveloperProfile != null) { + isProfileSelected = profiles.stream() + .anyMatch(profile -> { + return profile.getArn().equals(selectedDeveloperProfile.getArn()); + }); + + if (isProfileSelected && tryApplyCachedProfile) { + setDeveloperProfile(selectedDeveloperProfile, true, applyProfileUnconditionally); + } + } + return isProfileSelected; + } + + private List processConfigurations( + final LspServerConfigurations configurations) { + return Optional.ofNullable(configurations).map( + config -> { + return config.getConfigurations().stream().filter(this::isValidProfile) + .collect(Collectors.toList()); + }) + .orElse(Collections.emptyList()); + } + + public List getDeveloperProfiles() { + List profiles = getProfiles(); + if (profiles != null && !profiles.isEmpty()) { + return profiles; + } + + try { + return queryForDeveloperProfiles(false); + } catch (Exception e) { + Activator.getLogger().error("Interupted while fetching profiles: " + e); + } + + return null; + } + + public CompletableFuture setDeveloperProfile(final QDeveloperProfile developerProfile, + final boolean updateCustomization) { + return setDeveloperProfile(developerProfile, updateCustomization, false); + } + + private CompletableFuture setDeveloperProfile(final QDeveloperProfile developerProfile, + final boolean updateCustomization, final boolean applyProfileUnconditionally) { + if (developerProfile == null || (!applyProfileUnconditionally && selectedDeveloperProfile != null + && selectedDeveloperProfile.getArn().equals(developerProfile.getArn()))) { + return CompletableFuture.completedFuture(null); + } + + selectedDeveloperProfile = developerProfile; + saveSelectedProfile(); + + String section = "aws.q"; + Map settings = Map.of("profileArn", selectedDeveloperProfile.getArn()); + return Activator.getLspProvider().getAmazonQServer() + .thenCompose(server -> server.updateConfiguration(new UpdateConfigurationParams(section, settings))) + .thenRun(() -> { + showNotification(selectedDeveloperProfile.getName()); + Activator.getEventBroker().post(QDeveloperProfileState.class, QDeveloperProfileState.SELECTED); + if (profileSelectionTask != null) { + profileSelectionTask.complete(null); + } + setProfiles(null); + + Customization currentCustomization = Activator.getPluginStore() + .getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class); + + if (updateCustomization && currentCustomization != null + && !selectedDeveloperProfile.getArn().equals(currentCustomization.getProfile().getArn())) { + Activator.getPluginStore().remove(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY); + ThreadingUtils + .executeAsyncTask(() -> CustomizationUtil.triggerChangeConfigurationNotification()); + Display.getDefault().asyncExec( + () -> CustomizationUtil.showNotification(Constants.DEFAULT_Q_FOUNDATION_DISPLAY_NAME)); + } + }) + .exceptionally(throwable -> { + Activator.getLogger().error("Error occurred while setting Q Developer Profile: ", throwable); + throw new AmazonQPluginException(throwable); + }); + } + + private void showNotification(final String developerProfileName) { + Display.getDefault().asyncExec(() -> { + AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), + Constants.IDE_DEVELOPER_PROFILES_NOTIFICATION_TITLE, + String.format(Constants.IDE_DEVELOPER_PROFILES_NOTIFICATION_BODY_TEMPLATE, developerProfileName)); + notification.open(); + }); + } + + public void clearSelectedProfile() { + Activator.getPluginStore().remove(ViewConstants.Q_DEVELOPER_PROFILE_SELECTION_KEY); + selectedDeveloperProfile = null; + } + + private void saveSelectedProfile() { + try { + String serializedSelectedProfile = serializeProfile(selectedDeveloperProfile); + + if (serializedSelectedProfile != null) { + Activator.getPluginStore().put(ViewConstants.Q_DEVELOPER_PROFILE_SELECTION_KEY, + serializedSelectedProfile); + } + } catch (final JsonProcessingException e) { + Activator.getLogger().error("Failed to cache Q developer profile"); + } + } + + public boolean isProfileSelectionRequired() { + if (profiles == null || profiles.isEmpty()) { + try { + queryForDeveloperProfiles(false); + } catch (Exception e) { + Activator.getLogger().error("Interrupted when fetching profile: ", e); + } + + if (profiles.size() == 1) { + handleSingleOrNoProfile(profiles, true, false); + } + } + return profiles.size() > 1; + } + + public QDeveloperProfile getSelectedProfile() { + return selectedDeveloperProfile; + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtil.java deleted file mode 100644 index 3def241b4..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtil.java +++ /dev/null @@ -1,52 +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.customization; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -import org.eclipse.lsp4j.DidChangeConfigurationParams; - -import software.amazon.awssdk.utils.StringUtils; -import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; -import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; -import software.aws.toolkits.eclipse.amazonq.views.model.Customization; - -public final class CustomizationUtil { - - private CustomizationUtil() { - // to avoid initiation - } - - public static void triggerChangeConfigurationNotification() { - try { - Activator.getLogger().info("Triggering configuration pull from Amazon Q LSP server"); - Activator.getLspProvider().getAmazonQServer() - .thenAccept(server -> server.getWorkspaceService().didChangeConfiguration(new DidChangeConfigurationParams())); - } catch (Exception e) { - Activator.getLogger().error("Error occurred while sending change configuration notification to Amazon Q LSP server", e); - throw new AmazonQPluginException(e); - } - } - - public static CompletableFuture> listCustomizations() { - GetConfigurationFromServerParams params = new GetConfigurationFromServerParams(); - params.setSection("aws.q"); - return Activator.getLspProvider().getAmazonQServer() - .thenCompose(server -> server.getConfigurationFromServer(params)) - .thenApply(configurations -> Optional.ofNullable(configurations) - .map(config -> config.getCustomizations().stream() - .filter(customization -> customization != null && StringUtils.isNotBlank(customization.getName())) - .collect(Collectors.toList())) - .orElse(Collections.emptyList())) - .exceptionally(throwable -> { - Activator.getLogger().error("Error occurred while fetching the list of customizations", throwable); - throw new AmazonQPluginException(throwable); - }); - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/InMemoryInput.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/InMemoryInput.java new file mode 100644 index 000000000..17487580c --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/InMemoryInput.java @@ -0,0 +1,53 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.editor; + +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.IPersistableElement; +import org.eclipse.ui.IStorageEditorInput; + +public final class InMemoryInput implements IStorageEditorInput { + private final IStorage storage; + + public InMemoryInput(final IStorage storage) { + this.storage = storage; + } + + @Override + public IStorage getStorage() { + return storage; + } + + @Override + public boolean exists() { + return false; + } + + @Override + public String getName() { + return storage.getName(); + } + + @Override + public String getToolTipText() { + return getName(); + } + + @Override + public IPersistableElement getPersistable() { + return null; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return null; + } + + @Override + public T getAdapter(final Class adapter) { + return Platform.getAdapterManager().getAdapter(this, adapter); + } +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/MemoryStorage.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/MemoryStorage.java new file mode 100644 index 000000000..9846bca3f --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/editor/MemoryStorage.java @@ -0,0 +1,49 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.editor; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; + +public final class MemoryStorage implements IStorage { + private final String path; + private final byte[] bytes; + + public MemoryStorage(final String path, final String body) { + this.path = path; + this.bytes = body.getBytes(StandardCharsets.UTF_8); + } + + @Override + public InputStream getContents() { + return new ByteArrayInputStream(bytes.clone()); + } + + @Override + public IPath getFullPath() { + return new Path(path); + } + + @Override + public String getName() { + return path + " (preview)"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public T getAdapter(final Class adapter) { + return Platform.getAdapterManager().getAdapter(this, adapter); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/exception/LspError.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/exception/LspError.java index de8c8df31..ed039f967 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/exception/LspError.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/exception/LspError.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.exception; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/AbstractQChatEditorActionsHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/AbstractQChatEditorActionsHandler.java index 8cee0b4ac..a3bfe1a58 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/AbstractQChatEditorActionsHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/AbstractQChatEditorActionsHandler.java @@ -14,7 +14,6 @@ import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.schedulers.Schedulers; import software.aws.toolkits.eclipse.amazonq.broker.events.AmazonQLspState; -import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommand; import software.aws.toolkits.eclipse.amazonq.chat.models.GenericCommandParams; import software.aws.toolkits.eclipse.amazonq.chat.models.SendToPromptParams; @@ -102,7 +101,7 @@ private void sendToPromptCommand(final String selection) { ); ChatUIInboundCommand command = ChatUIInboundCommand.createSendToPromptCommand(params); - ChatCommunicationManager.getInstance().sendMessageToChatUI(command); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); } private void sendGenericCommand(final String selection, final String genericCommandVerb) { @@ -113,7 +112,7 @@ private void sendGenericCommand(final String selection, final String genericComm genericCommandVerb ); ChatUIInboundCommand command = ChatUIInboundCommand.createGenericCommand(params); - ChatCommunicationManager.getInstance().sendMessageToChatUI(command); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); } private void openQChat() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptInlineChatHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptInlineChatHandler.java index 5a298a438..31afa6518 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptInlineChatHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QAcceptInlineChatHandler.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.handlers; import org.eclipse.core.commands.AbstractHandler; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QRejectInlineChatHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QRejectInlineChatHandler.java index 44ba2f1c5..6a730dfb5 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QRejectInlineChatHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QRejectInlineChatHandler.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.handlers; import org.eclipse.core.commands.AbstractHandler; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerInlineChatHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerInlineChatHandler.java index 280688b63..31901070d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerInlineChatHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerInlineChatHandler.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.handlers; import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextEditor; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerSuggestionsHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerSuggestionsHandler.java index a065234fd..843f82607 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerSuggestionsHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/handlers/QTriggerSuggestionsHandler.java @@ -3,15 +3,16 @@ package software.aws.toolkits.eclipse.amazonq.handlers; +import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextEditor; + import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.QInvocationSession; -import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextEditor; - public class QTriggerSuggestionsHandler extends AbstractHandler { @Override @@ -23,7 +24,7 @@ public final boolean isEnabled() { @Override public final synchronized Object execute(final ExecutionEvent event) throws ExecutionException { var editor = getActiveTextEditor(); - if (editor == null) { + if (editor == null || editor.getEditorInput() instanceof InMemoryInput) { Activator.getLogger().info("Suggestion triggered with no active editor. Returning."); return null; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java index 0754d1158..5cf9c1dad 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import org.apache.commons.text.StringEscapeUtils; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; @@ -248,6 +249,10 @@ void restoreState() { } + void endSession() { + task = null; + } + private void setColorPalette(final boolean isDark) { this.annotationAdded = "diffAnnotation.added"; this.annotationDeleted = "diffAnnotation.deleted"; @@ -284,7 +289,6 @@ private String unescapeChatResult(final String s) { return s; } - return s.replace(""", "\"").replace("'", "'").replace("<", "<").replace("=<", "=<").replace("<=", "<=").replace(">", ">") - .replace("=>", "=>").replace(">=", ">=").replace(" ", " ").replace("‘", "'").replace("’", "'").replace("&", "&"); + return StringEscapeUtils.unescapeHtml4(s); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatEditorListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatEditorListener.java index 55f9ae123..588af99b0 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatEditorListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatEditorListener.java @@ -18,6 +18,7 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.ITextEditor; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; @@ -108,7 +109,8 @@ private void showPrompt(final ITextEditor editor, final ITextSelection selection Display.getDefault().asyncExec(() -> { try { // Check if we still have a valid selection before showing prompt - if (editor.getSelectionProvider().getSelection() instanceof ITextSelection) { + if (editor.getSelectionProvider().getSelection() instanceof ITextSelection + && !(editor.getEditorInput() instanceof InMemoryInput)) { ITextSelection currentSelection = (ITextSelection) editor.getSelectionProvider().getSelection(); // Only show if selection hasn't changed @@ -182,8 +184,11 @@ private void attachSelectionListener(final ITextEditor editor) { private void removeCurrentPaintListener() { + if (currentViewer == null || currentPaintListener == null) { + return; + } try { - if (currentViewer != null && !currentViewer.getTextWidget().isDisposed() && currentPaintListener != null) { + if (currentViewer.getTextWidget() != null && !currentViewer.getTextWidget().isDisposed()) { Display.getDefault().syncExec(() -> { currentViewer.getTextWidget().removePaintListener(currentPaintListener); currentViewer.getTextWidget().redraw(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatSession.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatSession.java index 567ca403d..76a3da908 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatSession.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatSession.java @@ -33,9 +33,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; +import software.aws.toolkits.eclipse.amazonq.chat.ChatMessage; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatPrompt; import software.aws.toolkits.eclipse.amazonq.chat.models.InlineChatRequestParams; import software.aws.toolkits.eclipse.amazonq.chat.models.InlineChatResult; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; @@ -97,7 +99,7 @@ public boolean startSession(final ITextEditor editor) { if (isSessionActive()) { return false; } - if (editor == null || !(editor instanceof ITextEditor)) { + if (editor == null || !(editor instanceof ITextEditor) || (editor.getEditorInput() instanceof InMemoryInput)) { return false; } try { @@ -155,7 +157,7 @@ private void start() { Activator.getLogger().info("Inline chat not submitted. Ending session."); } }).exceptionally(throwable -> { - Activator.getLogger().error("Failed to open user input prompt", throwable); + Activator.getLogger().error("Failed to open user input prompt: " + throwable.getMessage()); uiManager.showErrorNotification(); endSession(); return null; @@ -200,7 +202,7 @@ public void onSendToChatUi(final String message) { blockUserInput(false); } }).exceptionally(throwable -> { - Activator.getLogger().error("Failed to process diff", throwable); + Activator.getLogger().error("Failed to process diff: " + throwable.getMessage()); uiManager.showErrorNotification(); restoreAndEndSession(); return null; @@ -220,7 +222,7 @@ public void handleDecision(final boolean userAcceptedChanges) throws Exception { task.setUserDecision(userAcceptedChanges); endSession(); }).exceptionally(throwable -> { - Activator.getLogger().error("Failed to handle decision", throwable); + Activator.getLogger().error("Failed to handle decision: " + throwable.getMessage()); uiManager.showErrorNotification(); restoreAndEndSession(); return null; @@ -232,7 +234,7 @@ private void sendInlineChatRequest() { var prompt = task.getPrompt(); var chatPrompt = new ChatPrompt(prompt, prompt, "", Collections.emptyList()); params = new InlineChatRequestParams(chatPrompt, null, Arrays.asList(task.getCursorState())); - chatCommunicationManager.sendInlineChatMessageToChatServer(params); + chatCommunicationManager.sendInlineChatMessageToChatServer(new ChatMessage(params)); Optional fileUri = QEclipseEditorUtils.getOpenFileUri(); if (fileUri.isPresent()) { @@ -242,7 +244,7 @@ private void sendInlineChatRequest() { task.setRequestTime(System.currentTimeMillis()); } catch (Exception e) { - Activator.getLogger().error("Failed to send message to chat server: " + e.getMessage(), e); + Activator.getLogger().error("Failed to send message to chat server: " + e.getMessage()); endSession(); } } @@ -261,7 +263,7 @@ private synchronized void endSession() { removeFoldingListener(projectionModel); uiThreadFuture.complete(null); } catch (Exception e) { - Activator.getLogger().error("Error in UI cleanup: " + e.getMessage(), e); + Activator.getLogger().error("Error in UI cleanup: " + e.getMessage()); uiThreadFuture.completeExceptionally(e); } }); @@ -271,12 +273,15 @@ private synchronized void endSession() { var inlineChatSessionResult = task.buildResultObject(); emitInlineChatEventMetric(inlineChatSessionResult); } catch (Exception e) { - Activator.getLogger().error("FAILURE ON EMISSION:", e); + Activator.getLogger().error("FAILURE ON EMISSION: " + e.getMessage()); } - uiManager.closePrompt(); + uiManager.endSession(); + diffManager.endSession(); cleanupSessionState(); setState(SessionState.INACTIVE); Activator.getLogger().info("Inline chat session ended."); + }).thenRun(() -> { + task = null; }); } @@ -297,7 +302,7 @@ private CompletableFuture restoreState() { diffManager.restoreState(); future.complete(null); } catch (Exception e) { - Activator.getLogger().error("Error restoring editor state: " + e.getMessage(), e); + Activator.getLogger().error("Error restoring editor state: " + e.getMessage()); future.completeExceptionally(e); } }); @@ -414,7 +419,7 @@ private void cleanupSessionState() { ((ITextViewerExtension) viewer).removeVerifyKeyListener(verifyKeyListener); verifyKeyListener = null; } catch (Exception e) { - Activator.getLogger().error("Failed to remove verify key listener", e); + Activator.getLogger().error("Failed to remove verify key listener: " + e.getMessage()); } } } @@ -426,7 +431,7 @@ private void cleanupContext() { contextActivation = null; } } catch (Exception e) { - Activator.getLogger().error("Error cleaning up context: " + e.getMessage(), e); + Activator.getLogger().error("Error cleaning up context: " + e.getMessage()); } } @@ -437,7 +442,7 @@ private void cleanupWorkbench() { workbenchPage = null; } } catch (Exception e) { - Activator.getLogger().error("Failed to clean up part listener: " + e.getMessage(), e); + Activator.getLogger().error("Failed to clean up part listener: " + e.getMessage()); } } @@ -457,7 +462,7 @@ private void cleanupDocumentState(final boolean shouldRestoreState) { isCompoundChange = false; } } catch (Exception e) { - Activator.getLogger().error("Error cleaning up document state: " + e.getMessage(), e); + Activator.getLogger().error("Error cleaning up document state: " + e.getMessage()); } } @@ -478,7 +483,7 @@ private void createInlineChatTask(final ITextEditor editor) { final String selectionText = document.get(region.getOffset(), region.getLength()); task = new InlineChatTask(editor, selectionText, region, selectedLines); } catch (Exception e) { - Activator.getLogger().error("Failed to expand selection region: " + e.getMessage(), e); + Activator.getLogger().error("Failed to expand selection region: " + e.getMessage()); var region = new Region(selection.getOffset(), selection.getLength()); task = new InlineChatTask(editor, selection.getText(), region, selectedLines); } @@ -513,7 +518,7 @@ private IRegion expandSelectionToFullLines(final IDocument document, final IText return new Region(startRegion.getOffset(), selectionLength); } catch (Exception e) { - Activator.getLogger().error("Could not calculate line information: " + e.getMessage(), e); + Activator.getLogger().error("Could not calculate line information: " + e.getMessage()); return new Region(selection.getOffset(), selection.getLength()); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java index 530e33df7..2f24ebebc 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java @@ -45,6 +45,7 @@ public final class InlineChatUIManager { // UI elements private PopupDialog inputBox; private ITextViewer viewer; + private final int maxInputLength = 256; private PaintListener currentPaintListener; private final String inputPromptMessage = "Enter instructions for Amazon Q (Enter | Esc)"; private final String generatingMessage = "Amazon Q is generating..."; @@ -83,7 +84,6 @@ public CompletableFuture showUserInputPrompt() { } var widget = viewer.getTextWidget(); - inputBox = new PopupDialog(widget.getShell(), PopupDialog.INFOPOPUP_SHELLSTYLE, false, false, true, false, false, null, null) { private Point screenLocation; private Text inputField; @@ -168,6 +168,15 @@ public void keyPressed(final KeyEvent e) { gridData.widthHint = 350; inputField.setLayoutData(gridData); + // Enforce maximum character count that can be entered into the input + inputField.addVerifyListener(e -> { + String currentText = inputField.getText(); + String newText = currentText.substring(0, e.start) + e.text + currentText.substring(e.end); + if (newText.length() > maxInputLength) { + e.doit = false; // Prevent the input + } + }); + inputField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(final KeyEvent e) { @@ -321,6 +330,11 @@ void closePrompt() { }); } + void endSession() { + closePrompt(); + task = null; + } + private void removeCurrentPaintListener() { if (viewer == null) { return; @@ -350,7 +364,7 @@ private int calculateIndentOffset(final StyledText widget, final int currentOffs } private boolean userInputIsValid(final String input) { - return input != null && input.length() >= 2; + return input != null && input.length() >= 2 && input.length() < maxInputLength; } /** diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/TextDiff.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/TextDiff.java index fcb7e6c69..3db78685a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/TextDiff.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/TextDiff.java @@ -3,5 +3,5 @@ package software.aws.toolkits.eclipse.amazonq.inlineChat; -record TextDiff(int offset, int length, boolean isDeletion) { +public record TextDiff(int offset, int length, boolean isDeletion) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java index b8acbd5f5..3739f2358 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java @@ -9,8 +9,13 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageClient; +import software.aws.toolkits.eclipse.amazonq.chat.models.GetSerializedChatParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.SerializedChatResult; +import software.aws.toolkits.eclipse.amazonq.chat.models.ShowSaveFileDialogParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.ShowSaveFileDialogResult; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.SsoTokenChangedParams; import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata; +import software.aws.toolkits.eclipse.amazonq.lsp.model.OpenFileDiffParams; public interface AmazonQLspClient extends LanguageClient { @@ -20,4 +25,37 @@ public interface AmazonQLspClient extends LanguageClient { @JsonNotification("aws/identity/ssoTokenChanged") void ssoTokenChanged(SsoTokenChangedParams params); + @JsonNotification("aws/chat/sendContextCommands") + void sendContextCommands(Object params); + + @JsonRequest("aws/chat/openTab") + CompletableFuture openTab(Object params); + + @JsonRequest("aws/showSaveFileDialog") + CompletableFuture showSaveFileDialog(ShowSaveFileDialogParams params); + + @JsonRequest("aws/chat/getSerializedChat") + CompletableFuture getSerializedChat(GetSerializedChatParams params); + + @JsonNotification("aws/openFileDiff") + void openFileDiff(OpenFileDiffParams params); + + @JsonNotification("aws/chat/sendChatUpdate") + void sendChatUpdate(Object params); + + @JsonNotification("aws/didCopyFile") + void didCopyFile(Object params); + + @JsonNotification("aws/didWriteFile") + void didWriteFile(Object params); + + @JsonNotification("aws/didAppendFile") + void didAppendFile(Object params); + + @JsonNotification("aws/didRemoveFileOrDirectory") + void didRemoveFileOrDirectory(Object params); + + @JsonNotification("aws/didCreateDirectory") + void didCreateDirectory(Object params); + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java index 2498ee2c0..279a88efc 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java @@ -3,31 +3,82 @@ package software.aws.toolkits.eclipse.amazonq.lsp; -import java.net.MalformedURLException; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITextViewerExtension; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.lsp4e.LanguageClientImpl; import org.eclipse.lsp4j.ConfigurationParams; import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; -import org.eclipse.ui.PartInitException; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.VerifyKeyListener; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IPageListener; +import org.eclipse.ui.IStorageEditorInput; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment; +import software.aws.toolkits.eclipse.amazonq.chat.ChatAsyncResultManager; import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; +import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommand; +import software.aws.toolkits.eclipse.amazonq.chat.models.GetSerializedChatParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.GetSerializedChatResult; +import software.aws.toolkits.eclipse.amazonq.chat.models.SerializedChatResult; +import software.aws.toolkits.eclipse.amazonq.chat.models.ShowSaveFileDialogParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.ShowSaveFileDialogResult; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; +import software.aws.toolkits.eclipse.amazonq.editor.MemoryStorage; +import software.aws.toolkits.eclipse.amazonq.inlineChat.TextDiff; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.SsoTokenChangedKind; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.SsoTokenChangedParams; import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata; +import software.aws.toolkits.eclipse.amazonq.lsp.model.OpenFileDiffParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.OpenTabUiResponse; import software.aws.toolkits.eclipse.amazonq.lsp.model.SsoProfileData; import software.aws.toolkits.eclipse.amazonq.lsp.model.TelemetryEvent; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -35,12 +86,17 @@ import software.aws.toolkits.eclipse.amazonq.telemetry.service.DefaultTelemetryService; import software.aws.toolkits.eclipse.amazonq.util.Constants; import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; -import software.aws.toolkits.eclipse.amazonq.views.model.Customization; +import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; +import software.aws.toolkits.eclipse.amazonq.util.WorkspaceUtils; +import software.aws.toolkits.eclipse.amazonq.views.model.Customization; +import software.aws.toolkits.eclipse.amazonq.views.model.UpdateRedirectUrlCommand; @SuppressWarnings("restriction") public class AmazonQLspClientImpl extends LanguageClientImpl implements AmazonQLspClient { + private ThemeDetector themeDetector = new ThemeDetector(); + @Override public final CompletableFuture getConnectionMetadata() { return CompletableFuture.supplyAsync(() -> { @@ -81,6 +137,7 @@ public final CompletableFuture> configuration(final ConfigurationPa projectContextConfig.put(Constants.LSP_INDEX_THREADS_CONFIGURATION_KEY, indexThreadsSetting); qConfig.put(Constants.LSP_PROJECT_CONTEXT_CONFIGURATION_KEY, projectContextConfig); output.add(qConfig); + Activator.getLspProvider().activate(AmazonQLspServer.class); } else if (item.getSection().equals(Constants.LSP_CW_CONFIGURATION_KEY)) { Map cwConfig = new HashMap<>(); boolean shareContentSetting = Activator.getDefault().getPreferenceStore().getBoolean(AmazonQPreferencePage.Q_DATA_SHARING); @@ -145,14 +202,42 @@ private void sendFeedback(final TelemetryEvent telemetryEvent) { @Override public final CompletableFuture showDocument(final ShowDocumentParams params) { - Activator.getLogger().info("Opening redirect URL: " + params.getUri()); + String uri = params.getUri(); + Activator.getLogger().info("Opening URI: " + uri); + return CompletableFuture.supplyAsync(() -> { - try { - PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(params.getUri())); - return new ShowDocumentResult(true); - } catch (PartInitException | MalformedURLException e) { - Activator.getLogger().error("Error opening URL: " + params.getUri(), e); - return new ShowDocumentResult(false); + final boolean[] success = new boolean[1]; + if (params.getExternal() != null && params.getExternal()) { + var command = new UpdateRedirectUrlCommand(uri); + Activator.getEventBroker().post(UpdateRedirectUrlCommand.class, command); + Display.getDefault().syncExec(() -> { + try { + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(uri)); + success[0] = true; + } catch (Exception e) { + Activator.getLogger().error("Error in UI thread while opening external URI: " + uri, e); + success[0] = false; + } + }); + return new ShowDocumentResult(success[0]); + } else { + Display.getDefault().syncExec(() -> { + try { + if (!isUriInWorkspace(uri)) { + Activator.getLogger().error("Attempted to open file outside workspace: " + uri); + success[0] = false; + } else { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IFileStore fileStore = EFS.getLocalFileSystem().getStore(new URI(uri)); + IDE.openEditorOnFileStore(page, fileStore); + success[0] = true; + } + } catch (Exception e) { + Activator.getLogger().error("Error in UI thread while opening URI: " + uri, e); + success[0] = false; + } + }); + return new ShowDocumentResult(success[0]); } }); } @@ -179,4 +264,308 @@ public final void ssoTokenChanged(final SsoTokenChangedParams params) { } } + @Override + public final void sendContextCommands(final Object params) { + var command = ChatUIInboundCommand.createCommand("aws/chat/sendContextCommands", params); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); + } + + @Override + public final CompletableFuture openTab(final Object params) { + return CompletableFuture.supplyAsync(() -> { + String requestId = UUID.randomUUID().toString(); + var command = ChatUIInboundCommand.createCommand("aws/chat/openTab", params, requestId); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); + ChatAsyncResultManager manager = ChatAsyncResultManager.getInstance(); + manager.createRequestId(requestId); + OpenTabUiResponse response; + try { + Object res = ChatAsyncResultManager.getInstance().getResult(requestId); + response = ObjectMapperFactory.getInstance().convertValue(res, OpenTabUiResponse.class); + } catch (Exception e) { + throw new IllegalStateException("Failed to retrieve new tab response from chat UI", e); + } finally { + manager.removeRequestId(requestId); + } + if (response.result() == null) { + Activator.getLogger().warn("Got null tab response from UI"); + return null; + } + return response.result(); + }); + } + + @Override + public final CompletableFuture showSaveFileDialog(final ShowSaveFileDialogParams params) { + CompletableFuture future = new CompletableFuture<>(); + Display.getDefault().syncExec(() -> { + String name = "export-chat.md"; + String path = ""; + try { + URI uri = new URI(params.defaultUri()); + File file = new File(uri); + path = file.getParent(); + name = file.getName(); + } catch (URISyntaxException e) { + Activator.getLogger().warn("Unable to parse file path details from showSaveFileDialog params: " + e.getMessage()); + } + + Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + FileDialog dialog = new FileDialog(shell, SWT.SAVE); + dialog.setFilterExtensions(new String[] {"*.md", "*.html"}); + dialog.setFilterPath(path); + dialog.setFileName(name); + dialog.setFilterNames(new String[] {"Markdown Files (.md)", "HTML Files (.html)"}); + dialog.setOverwrite(true); + + String filePath = dialog.open(); + if (filePath != null) { + future.complete(new ShowSaveFileDialogResult(filePath)); + } else { + future.completeExceptionally(new IllegalStateException("User did not provide file path for export")); + } + }); + return future; + } + + @Override + public final CompletableFuture getSerializedChat(final GetSerializedChatParams params) { + return CompletableFuture.supplyAsync(() -> { + String requestId = UUID.randomUUID().toString(); + var command = ChatUIInboundCommand.createCommand("aws/chat/getSerializedChat", params, requestId); + ChatAsyncResultManager manager = ChatAsyncResultManager.getInstance(); + manager.createRequestId(requestId); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); + SerializedChatResult result; + try { + Object res = ChatAsyncResultManager.getInstance().getResult(requestId); + GetSerializedChatResult serializedChatResult = ObjectMapperFactory.getInstance().convertValue(res, GetSerializedChatResult.class); + result = serializedChatResult.result(); + } catch (Exception e) { + throw new IllegalStateException("Failed to retrieve serialized chat from chat UI", e); + } finally { + manager.removeRequestId(requestId); + } + return result; + }); + } + + @Override + public final void openFileDiff(final OpenFileDiffParams params) { + String annotationAdded = themeDetector.isDarkTheme() ? "diffAnnotation.added.dark" : "diffAnnotation.added"; + String annotationDeleted = themeDetector.isDarkTheme() ? "diffAnnotation.deleted.dark" + : "diffAnnotation.deleted"; + + Display.getDefault().asyncExec(() -> { + try { + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IStorageEditorInput input = new InMemoryInput( + new MemoryStorage(new Path(params.originalFileUri().getPath()).lastSegment(), "")); + + IEditorDescriptor defaultEditor = PlatformUI.getWorkbench().getEditorRegistry() + .getDefaultEditor(".java"); + + IEditorPart editor = page.openEditor(input, + defaultEditor != null ? defaultEditor.getId() : "org.eclipse.ui.DefaultTextEditor", true, + IWorkbenchPage.MATCH_INPUT); + // Annotation model provides highlighting for the diff additions/deletions + IAnnotationModel annotationModel = ((ITextEditor) editor).getDocumentProvider() + .getAnnotationModel(editor.getEditorInput()); + var document = ((ITextEditor) editor).getDocumentProvider().getDocument(editor.getEditorInput()); + + // Split original and new code into lines for diff comparison + String[] originalLines = (params.originalFileContent() != null + && !params.originalFileContent().isEmpty()) + ? params.originalFileContent().lines().toArray(String[]::new) + : new String[0]; + String[] newLines = (params.fileContent() != null && !params.fileContent().isEmpty()) + ? params.fileContent().lines().toArray(String[]::new) + : new String[0]; + // Diff generation --> returns Patch object which contains deltas for each line + Patch patch = DiffUtils.diff(Arrays.asList(originalLines), Arrays.asList(newLines)); + + StringBuilder resultText = new StringBuilder(); + List currentDiffs = new ArrayList<>(); + int currentPos = 0; + int currentLine = 0; + + for (AbstractDelta delta : patch.getDeltas()) { + // Continuously copy unchanged lines until we hit a diff + while (currentLine < delta.getSource().getPosition()) { + resultText.append(originalLines[currentLine]).append("\n"); + currentPos += originalLines[currentLine].length() + 1; + currentLine++; + } + + List originalChangedLines = delta.getSource().getLines(); + List newChangedLines = delta.getTarget().getLines(); + + // Handle deleted lines and mark position + for (String line : originalChangedLines) { + resultText.append(line).append("\n"); + currentDiffs.add(new TextDiff(currentPos, line.length(), true)); + currentPos += line.length() + 1; + } + + // Handle added lines and mark position + for (String line : newChangedLines) { + resultText.append(line).append("\n"); + currentDiffs.add(new TextDiff(currentPos, line.length(), false)); + currentPos += line.length() + 1; + } + + currentLine = delta.getSource().getPosition() + delta.getSource().size(); + } + // Loop through remaining unchanged lines + while (currentLine < originalLines.length) { + resultText.append(originalLines[currentLine]).append("\n"); + currentPos += originalLines[currentLine].length() + 1; + currentLine++; + } + + final String finalText = resultText.toString(); + document.replace(0, document.getLength(), finalText); + + // Add all annotations after text modifications are complete + for (TextDiff diff : currentDiffs) { + Position position = new Position(diff.offset(), diff.length()); + String annotationType = diff.isDeletion() ? annotationDeleted : annotationAdded; + String annotationText = diff.isDeletion() ? "Deleted Code" : "Added Code"; + annotationModel.addAnnotation(new Annotation(annotationType, false, annotationText), position); + } + makeEditorReadOnly(editor); + } catch (CoreException | BadLocationException e) { + Activator.getLogger().info("Failed to open file/diff: " + e); + } + }); + } + + private void makeEditorReadOnly(final IEditorPart editor) { + ITextViewer viewer = editor.getAdapter(ITextViewer.class); + if (viewer != null) { + VerifyKeyListener verifyKeyListener = event -> event.doit = false; + ((ITextViewerExtension) viewer).prependVerifyKeyListener(verifyKeyListener); + } + + // stop text‑modifying commands + ActionFactory[] ids = {ActionFactory.UNDO, ActionFactory.REDO, ActionFactory.CUT, ActionFactory.PASTE, + ActionFactory.DELETE}; + for (ActionFactory id : ids) { + IAction a = ((ITextEditor) editor).getAction(id.getId()); + if (a != null) { + a.setEnabled(false); + } + } + + IWorkbenchPartSite site = editor.getSite(); + if (site == null) { + return; + } + + IWorkbenchWindow window = site.getWorkbenchWindow(); + if (window == null) { + return; + } + + Runnable cleanupEditor = () -> { + Display.getDefault().asyncExec(() -> { + try { + if (editor != null && !editor.isDirty()) { + IWorkbenchPage currentPage = editor.getSite().getPage(); + if (currentPage != null) { + // Remove annotations + if (editor instanceof ITextEditor) { + ITextEditor textEditor = (ITextEditor) editor; + IDocumentProvider provider = textEditor.getDocumentProvider(); + if (provider != null) { + IAnnotationModel annotationModel = provider + .getAnnotationModel(editor.getEditorInput()); + if (annotationModel != null) { + Iterator annotationIterator = annotationModel.getAnnotationIterator(); + while (annotationIterator.hasNext()) { + annotationModel.removeAnnotation((Annotation) annotationIterator.next()); + } + } + } + } + } + } + } catch (Exception e) { + Activator.getLogger().error("Error during editor cleanup", e); + } + }); + }; + + IPageListener pageListener = new IPageListener() { + @Override + public void pageOpened(final IWorkbenchPage page) { + } + + @Override + public void pageClosed(final IWorkbenchPage page) { + cleanupEditor.run(); + window.removePageListener(this); + } + + @Override + public void pageActivated(final IWorkbenchPage page) { + } + }; + + window.addPageListener(pageListener); + + editor.doSave(new NullProgressMonitor()); + } + + @Override + public final void sendChatUpdate(final Object params) { + var conversationClickCommand = new ChatUIInboundCommand("aws/chat/sendChatUpdate", null, params, + false, null); + Activator.getEventBroker().post(ChatUIInboundCommand.class, conversationClickCommand); + } + + @Override + public final void didCopyFile(final Object params) { + refreshProjects(); + } + + @Override + public final void didWriteFile(final Object params) { + refreshProjects(); + } + + @Override + public final void didAppendFile(final Object params) { + refreshProjects(); + } + + @Override + public final void didRemoveFileOrDirectory(final Object params) { + refreshProjects(); + } + + @Override + public final void didCreateDirectory(final Object params) { + refreshProjects(); + } + + private void refreshProjects() { + WorkspaceUtils.refreshAllProjects(); + } + + private boolean isUriInWorkspace(final String uri) { + try { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IPath workspacePath = workspace.getRoot().getLocation(); + + URI fileUri = new URI(uri); + File file = new File(fileUri); + String filePath = file.getCanonicalPath(); + + return filePath.startsWith(workspacePath.toFile().getCanonicalPath()); + } catch (Exception e) { + Activator.getLogger().error("Error validating URI location: " + uri, e); + return false; + } + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java index 9b990ae3a..22a3ec5e7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java @@ -3,16 +3,12 @@ package software.aws.toolkits.eclipse.amazonq.lsp; import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; -import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; + import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; import org.eclipse.lsp4j.services.LanguageServer; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; +import software.aws.toolkits.eclipse.amazonq.chat.models.ButtonClickResult; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.GetSsoTokenParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.GetSsoTokenResult; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.InvalidateSsoTokenParams; @@ -20,62 +16,90 @@ import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.ListProfilesResult; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.UpdateProfileParams; import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; -import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionParams; import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionResponse; -import software.aws.toolkits.eclipse.amazonq.lsp.model.LogInlineCompletionSessionResultsParams; import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; +import software.aws.toolkits.eclipse.amazonq.views.model.Configuration; +import software.aws.toolkits.eclipse.amazonq.views.model.UpdateConfigurationParams; public interface AmazonQLspServer extends LanguageServer { @JsonRequest("aws/textDocument/inlineCompletionWithReferences") - CompletableFuture inlineCompletionWithReferences(InlineCompletionParams params); + CompletableFuture inlineCompletionWithReferences(Object params); @JsonNotification("aws/logInlineCompletionSessionResults") - void logInlineCompletionSessionResult(LogInlineCompletionSessionResultsParams params); + void logInlineCompletionSessionResult(Object params); @JsonRequest("aws/chat/sendChatPrompt") - CompletableFuture sendChatPrompt(EncryptedChatParams encryptedChatRequestParams); + CompletableFuture sendChatPrompt(Object encryptedChatRequestParams); @JsonRequest("aws/chat/sendInlineChatPrompt") - CompletableFuture sendInlineChatPrompt(EncryptedChatParams encryptedChatRequestParams); + CompletableFuture sendInlineChatPrompt(Object encryptedChatRequestParams); @JsonRequest("aws/chat/sendChatQuickAction") - CompletableFuture sendQuickAction(EncryptedQuickActionParams encryptedQuickActionParams); + CompletableFuture sendQuickAction(Object encryptedQuickActionParams); @JsonRequest("aws/chat/endChat") - CompletableFuture endChat(GenericTabParams params); + CompletableFuture endChat(Object params); @JsonNotification("aws/chat/tabAdd") - void tabAdd(GenericTabParams params); + void tabAdd(Object params); @JsonNotification("aws/chat/tabRemove") - void tabRemove(GenericTabParams params); + void tabRemove(Object params); @JsonNotification("aws/chat/tabChange") - void tabChange(GenericTabParams params); + void tabChange(Object params); + + @JsonNotification("aws/chat/fileClick") + void fileClick(Object params); + + @JsonNotification("aws/chat/infoLinkClick") + void infoLinkClick(Object params); + + @JsonNotification("aws/chat/linkClick") + void linkClick(Object params); + + @JsonNotification("aws/chat/sourceLinkClick") + void sourceLinkClick(Object params); @JsonNotification("aws/chat/followUpClick") - void followUpClick(FollowUpClickParams params); + void followUpClick(Object params); + + @JsonNotification("aws/chat/promptInputOptionChange") + void promptInputOptionChange(Object params); @JsonNotification("aws/chat/ready") void chatReady(); @JsonNotification("aws/chat/feedback") - void sendFeedback(FeedbackParams params); + void sendFeedback(Object params); + + @JsonNotification("aws/chat/insertToCursorPosition") + void insertToCursorPosition(Object params); @JsonRequest("aws/credentials/token/update") - CompletableFuture updateTokenCredentials(UpdateCredentialsPayload payload); + CompletableFuture updateTokenCredentials(UpdateCredentialsPayload payload); @JsonNotification("aws/credentials/token/delete") void deleteTokenCredentials(); @JsonRequest("aws/getConfigurationFromServer") - CompletableFuture getConfigurationFromServer(GetConfigurationFromServerParams params); + CompletableFuture> getConfigurationFromServer( + GetConfigurationFromServerParams params); + + @JsonRequest("aws/updateConfiguration") + CompletableFuture updateConfiguration(UpdateConfigurationParams params); @JsonNotification("telemetry/event") void sendTelemetryEvent(Object params); + @JsonRequest("aws/chat/listConversations") + CompletableFuture listConversations(Object params); + + @JsonRequest("aws/chat/conversationClick") + CompletableFuture conversationClick(Object params); + @JsonRequest("aws/identity/listProfiles") CompletableFuture listProfiles(); @@ -87,4 +111,16 @@ public interface AmazonQLspServer extends LanguageServer { @JsonRequest("aws/identity/updateProfile") CompletableFuture updateProfile(UpdateProfileParams params); + + @JsonNotification("aws/chat/createPrompt") + CompletableFuture createPrompt(Object params); + + @JsonRequest("aws/chat/tabBarAction") + CompletableFuture tabBarAction(Object params); + + @JsonRequest("aws/chat/getSerializedChat") + CompletableFuture getSerializedActions(Object params); + + @JsonRequest("aws/chat/buttonClick") + CompletableFuture buttonClick(Object params); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java index e28df6eb7..74c192181 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Map; + import org.eclipse.lsp4j.ClientInfo; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.jsonrpc.Launcher; @@ -16,6 +17,7 @@ import com.google.gson.ToNumberPolicy; +import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommand; import software.aws.toolkits.eclipse.amazonq.lsp.model.AwsExtendedInitializeResult; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ClientMetadata; @@ -42,6 +44,8 @@ private Map getInitializationOptions(final ClientMetadata metada Map awsInitOptions = new HashMap<>(); Map extendedClientInfoOptions = new HashMap<>(); Map extensionOptions = new HashMap<>(); + Map awsClientCapabilities = new HashMap<>(); + Map qOptions = new HashMap<>(); extensionOptions.put("name", USER_AGENT_CLIENT_NAME); extensionOptions.put("version", metadata.getPluginVersion()); extendedClientInfoOptions.put("extension", extensionOptions); @@ -49,13 +53,20 @@ private Map getInitializationOptions(final ClientMetadata metada extendedClientInfoOptions.put("version", metadata.getIdeVersion()); extendedClientInfoOptions.put("name", metadata.getIdeName()); awsInitOptions.put("clientInfo", extendedClientInfoOptions); + qOptions.put("developerProfiles", true); + qOptions.put("customizationsWithMetadata", true); + awsClientCapabilities.put("q", qOptions); + Map window = new HashMap<>(); + window.put("showSaveFileDialog", true); + awsClientCapabilities.put("window", window); + awsInitOptions.put("awsClientCapabilities", awsClientCapabilities); initOptions.put("aws", awsInitOptions); return initOptions; } @Override protected final MessageConsumer wrapMessageConsumer(final MessageConsumer consumer) { - return super.wrapMessageConsumer((Message message) -> { + return super.wrapMessageConsumer((final Message message) -> { if (message instanceof RequestMessage && ((RequestMessage) message).getMethod().equals("initialize")) { InitializeParams initParams = (InitializeParams) ((RequestMessage) message).getParams(); ClientMetadata metadata = PluginClientMetadata.getInstance(); @@ -64,9 +75,9 @@ protected final MessageConsumer wrapMessageConsumer(final MessageConsumer consum } if (message instanceof ResponseMessage && ((ResponseMessage) message).getResult() instanceof AwsExtendedInitializeResult) { AwsExtendedInitializeResult result = (AwsExtendedInitializeResult) ((ResponseMessage) message).getResult(); - var awsServerCapabiltiesProvider = AwsServerCapabiltiesProvider.getInstance(); - awsServerCapabiltiesProvider.setAwsServerCapabilties(result.getAwsServerCapabilities()); - Activator.getLspProvider().setAmazonQServer(launcher.getRemoteProxy()); + var command = ChatUIInboundCommand.createCommand("chatOptions", result.getAwsServerCapabilities().chatOptions()); + Activator.getEventBroker().post(ChatUIInboundCommand.class, command); + Activator.getLspProvider().setServer(AmazonQLspServer.class, launcher.getRemoteProxy()); } consumer.consume(message); }); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AwsServerCapabiltiesProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AwsServerCapabiltiesProvider.java deleted file mode 100644 index 5e9a5bea5..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AwsServerCapabiltiesProvider.java +++ /dev/null @@ -1,61 +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.lsp; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import software.aws.toolkits.eclipse.amazonq.lsp.model.AwsServerCapabilities; -import software.aws.toolkits.eclipse.amazonq.lsp.model.ChatOptions; -import software.aws.toolkits.eclipse.amazonq.lsp.model.Command; -import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActions; -import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; - -public final class AwsServerCapabiltiesProvider { - private static AwsServerCapabiltiesProvider instance; - private AwsServerCapabilities serverCapabilties; - private static final ChatOptions DEFAULT_CHAT_OPTIONS = new ChatOptions( - new QuickActions( - Collections.singletonList( - new QuickActionsCommandGroup( - Arrays.asList( - new Command("/help", "Learn more about Amazon Q"), - new Command("/clear", "Clear this session") - ) - ) - ) - ) - ); - - private static final List DEFAULT_CONTEXT_COMMANDS = Collections.singletonList( - new QuickActionsCommandGroup( - Arrays.asList( - new Command("@workspace", "Reference all code in workspace.") - ) - ) - ); - - public static synchronized AwsServerCapabiltiesProvider getInstance() { - if (instance == null) { - instance = new AwsServerCapabiltiesProvider(); - } - return instance; - } - - public void setAwsServerCapabilties(final AwsServerCapabilities serverCapabilties) { - this.serverCapabilties = serverCapabilties; - } - - public ChatOptions getChatOptions() { - if (serverCapabilties != null) { - return serverCapabilties.chatOptions(); - } - return DEFAULT_CHAT_OPTIONS; - } - - public List getContextCommands() { - return DEFAULT_CONTEXT_COMMANDS; - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java index 3a1006ec3..8e9d0cbac 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/LspStartupActivity.java @@ -17,7 +17,9 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import software.aws.toolkits.eclipse.amazonq.broker.events.QDeveloperProfileState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.providers.browser.AmazonQBrowserProvider; import software.aws.toolkits.eclipse.amazonq.telemetry.ToolkitTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata; import software.aws.toolkits.eclipse.amazonq.util.AutoTriggerDocumentListener; @@ -44,6 +46,9 @@ protected IStatus run(final IProgressMonitor monitor) { startLspServer(); Display.getDefault().asyncExec(() -> { AmazonQToolbarActions.getInstance(); + AmazonQBrowserProvider.getInstance().publishBrowserCompatibilityState(); + Activator.getEventBroker().post(QDeveloperProfileState.class, + QDeveloperProfileState.NOT_APPLICABLE); }); Activator.getLspProvider().getAmazonQServer().thenAcceptAsync(server -> { try { @@ -84,7 +89,9 @@ private void checkForUpdates() { @Override protected IStatus run(final IProgressMonitor monitor) { try { - UpdateUtils.getInstance().checkForUpdate(); + Activator.getLspProvider().getAmazonQServer().thenAcceptAsync(server -> { + UpdateUtils.getInstance().checkForUpdate(); + }, ThreadingUtils.getWorkerPool()); } catch (Exception e) { return new Status(IStatus.WARNING, "amazonq", "Failed to check for updates", e); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java index ea055ab6e..f04864e8a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java @@ -4,10 +4,16 @@ package software.aws.toolkits.eclipse.amazonq.lsp; import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; import org.eclipse.lsp4j.InitializeResult; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; @@ -15,14 +21,51 @@ import com.google.gson.stream.JsonWriter; import software.aws.toolkits.eclipse.amazonq.lsp.model.AwsExtendedInitializeResult; +import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; +import software.aws.toolkits.eclipse.amazonq.views.model.Configuration; +import software.aws.toolkits.eclipse.amazonq.views.model.Customization; +import software.aws.toolkits.eclipse.amazonq.views.model.QDeveloperProfile; public class QLspTypeAdapterFactory implements TypeAdapterFactory { @Override @SuppressWarnings("unchecked") public final TypeAdapter create(final Gson gson, final TypeToken type) { + if (type.getRawType() == LspServerConfigurations.class) { + return (TypeAdapter) new TypeAdapter>() { + @Override + public void write(final JsonWriter out, final LspServerConfigurations value) + throws IOException { + gson.toJson(value.getConfigurations(), new TypeToken>() { + }.getType(), out); + } + + @Override + public LspServerConfigurations read(final JsonReader in) throws IOException { + JsonElement rootElement = JsonParser.parseReader(in); + + List customizations = new ArrayList<>(); + + if (rootElement.isJsonArray()) { + JsonArray array = rootElement.getAsJsonArray(); + Type listType = TypeToken.getParameterized(List.class, QDeveloperProfile.class).getType(); + + if (!array.isEmpty() && array.get(0).isJsonObject() + && array.get(0).getAsJsonObject().has("description")) { + listType = TypeToken.getParameterized(List.class, Customization.class).getType(); + } + + customizations = gson.fromJson(rootElement.getAsJsonArray(), listType); + } + + return new LspServerConfigurations<>(customizations); + } + }.nullSafe(); + } + if (type.getRawType() == InitializeResult.class) { - final TypeAdapter delegate = (TypeAdapter) gson.getDelegateAdapter(this, type); + final TypeAdapter delegate = (TypeAdapter) gson.getDelegateAdapter(this, + type); return (TypeAdapter) new TypeAdapter() { @Override diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthCredentialsService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthCredentialsService.java index 0b572087a..f343e86c5 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthCredentialsService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthCredentialsService.java @@ -5,11 +5,9 @@ import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; - import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; public interface AuthCredentialsService { - CompletableFuture updateTokenCredentials(UpdateCredentialsPayload params); + CompletableFuture updateTokenCredentials(UpdateCredentialsPayload params); CompletableFuture deleteTokenCredentials(); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsService.java index a28cf7532..faea04ec4 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsService.java @@ -6,8 +6,6 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; - import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -25,7 +23,7 @@ public static Builder builder() { } @Override - public CompletableFuture updateTokenCredentials(final UpdateCredentialsPayload params) { + public CompletableFuture updateTokenCredentials(final UpdateCredentialsPayload params) { return lspProvider.getAmazonQServer() .thenCompose(server -> server.updateTokenCredentials(params)) .exceptionally(throwable -> { @@ -42,15 +40,15 @@ public CompletableFuture deleteTokenCredentials() { }); } - public static class Builder { + public static final class Builder { private LspProvider lspProvider; - public final Builder withLspProvider(final LspProvider lspProvider) { + public Builder withLspProvider(final LspProvider lspProvider) { this.lspProvider = lspProvider; return this; } - public final DefaultAuthCredentialsService build() { + public DefaultAuthCredentialsService build() { if (lspProvider == null) { lspProvider = Activator.getLspProvider(); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java index 0703c072b..421b6f1c7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java @@ -4,6 +4,7 @@ package software.aws.toolkits.eclipse.amazonq.lsp.auth; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; +import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthStateType; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginParams; @@ -49,7 +50,8 @@ public DefaultAuthStateManager(final PluginStore pluginStore) { } @Override - public void toLoggedIn(final LoginType loginType, final LoginParams loginParams, final String ssoTokenId) throws IllegalArgumentException { + public void toLoggedIn(final LoginType loginType, final LoginParams loginParams, final String ssoTokenId) + throws IllegalArgumentException { if (loginType == null) { throw new IllegalArgumentException("loginType is a required parameter"); } @@ -66,8 +68,8 @@ public void toLoggedIn(final LoginType loginType, final LoginParams loginParams, throw new IllegalArgumentException("ssoTokenId is a required parameter"); } - updateState(AuthStateType.LOGGED_IN, loginType, loginParams, ssoTokenId); + } @Override @@ -121,7 +123,13 @@ private void updateState(final AuthStateType authStatusType, final LoginType log */ AuthState newAuthState = getAuthState(); if (previousAuthState == null || newAuthState.authStateType() != previousAuthState.authStateType()) { - Activator.getEventBroker().post(AuthState.class, newAuthState); + if (loginType == LoginType.IAM_IDENTITY_CENTER && newAuthState.isLoggedIn()) { + QDeveloperProfileUtil.getInstance().getProfileSelectionTaskFuture().thenRun(() -> { + Activator.getEventBroker().post(AuthState.class, newAuthState); + }); + } else { + Activator.getEventBroker().post(AuthState.class, newAuthState); + } } previousAuthState = newAuthState; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthTokenService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthTokenService.java index 1639c684c..0733653b9 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthTokenService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthTokenService.java @@ -94,15 +94,15 @@ private GetSsoTokenParams createGetSsoTokenParams(final LoginType currentLogin, return new GetSsoTokenParams(source, AWSProduct.AMAZON_Q_FOR_ECLIPSE.toString(), options); } - public static class Builder { + public static final class Builder { private LspProvider lspProvider; - public final Builder withLspProvider(final LspProvider lspProvider) { + public Builder withLspProvider(final LspProvider lspProvider) { this.lspProvider = lspProvider; return this; } - public final DefaultAuthTokenService build() { + public DefaultAuthTokenService build() { if (lspProvider == null) { lspProvider = Activator.getLspProvider(); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginService.java index 63d9d7c05..f1c077e43 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginService.java @@ -8,7 +8,8 @@ import java.util.concurrent.atomic.AtomicReference; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; -import software.aws.toolkits.eclipse.amazonq.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.InvalidateSsoTokenParams; @@ -50,7 +51,9 @@ private DefaultLoginService(final Builder builder) { AuthState authState = authStateManager.getAuthState(); if (!authState.isLoggedOut()) { boolean loginOnInvalidToken = false; - reAuthenticate(loginOnInvalidToken); + reAuthenticate(loginOnInvalidToken).thenRun(() -> { + QDeveloperProfileUtil.getInstance().initialize(); + }); } } } @@ -94,6 +97,7 @@ public CompletableFuture logout() { Activator.getLogger().info("Attempting to log out..."); InvalidateSsoTokenParams params = new InvalidateSsoTokenParams(authState.ssoTokenId()); + QDeveloperProfileUtil.getInstance().clearSelectedProfile(); return authTokenService.invalidateSsoToken(params) .thenRun(() -> { @@ -113,7 +117,7 @@ public CompletableFuture logout() { public CompletableFuture expire() { Activator.getLogger().info("Attempting to expire credentials..."); - return authCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(null, false)) + return authCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(null, null, false)) .thenRun(() -> { authStateManager.toExpired(); Activator.getLogger().info("Successfully expired credentials"); @@ -159,16 +163,19 @@ CompletableFuture processLogin(final LoginType loginType, final LoginParam return ssoToken; }) .thenAccept(ssoToken -> { - authCredentialsService.updateTokenCredentials(ssoToken.updateCredentialsParams()); + authCredentialsService.updateTokenCredentials(loginType == LoginType.IAM_IDENTITY_CENTER + ? ssoToken.getUpdateCredentialsPayloadHydratedWithStartUrl( + loginParams.getLoginIdcParams().getUrl()) + : ssoToken.updateCredentialsParams()); }) .thenRun(() -> { authStateManager.toLoggedIn(loginType, loginParams, ssoTokenId.get()); Activator.getLogger().info("Successfully logged in"); + }).thenRun(() -> { CustomizationUtil.triggerChangeConfigurationNotification(); - }) - .exceptionally(throwable -> { - throw new AmazonQPluginException("Failed to process log in", throwable); - }); + }).exceptionally(throwable -> { + throw new AmazonQPluginException("Failed to process log in", throwable); + }); } public static class Builder { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/AuthState.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/AuthState.java index 1726205f7..1a34ff015 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/AuthState.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/AuthState.java @@ -13,6 +13,10 @@ public record AuthState( @JsonProperty("ssoTokenId") String ssoTokenId ) { + public AuthState(final AuthStateType authStateType, final LoginType loginType) { + this(authStateType, loginType, null, null, null); + } + public Boolean isLoggedIn() { return authStateType.equals(AuthStateType.LOGGED_IN); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenError.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenError.java deleted file mode 100644 index fb7be8a99..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenError.java +++ /dev/null @@ -1,17 +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.lsp.auth.model; - -@SuppressWarnings("serial") -public class GetSsoTokenError extends RuntimeException { - private ErrorCode errorCode; - - public final ErrorCode getErrorCode() { - return errorCode; - } - - public final void setErrorCode(final ErrorCode errorCode) { - this.errorCode = errorCode; - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenResult.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenResult.java index f2e95ec97..7232f2387 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenResult.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/GetSsoTokenResult.java @@ -3,6 +3,19 @@ package software.aws.toolkits.eclipse.amazonq.lsp.auth.model; +import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata; +import software.aws.toolkits.eclipse.amazonq.lsp.model.SsoProfileData; import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; -public record GetSsoTokenResult(SsoToken ssoToken, UpdateCredentialsPayload updateCredentialsParams) { } +public record GetSsoTokenResult(SsoToken ssoToken, UpdateCredentialsPayload updateCredentialsParams) { + + public UpdateCredentialsPayload getUpdateCredentialsPayloadHydratedWithStartUrl(final String startUrl) { + SsoProfileData ssoProfileData = new SsoProfileData(); + ssoProfileData.setStartUrl(startUrl); + ConnectionMetadata metadata = new ConnectionMetadata(); + metadata.setSso(ssoProfileData); + return new UpdateCredentialsPayload(updateCredentialsParams.data(), metadata, + updateCredentialsParams.encrypted()); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/LoginDetails.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/LoginDetails.java deleted file mode 100644 index e87e373c5..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/model/LoginDetails.java +++ /dev/null @@ -1,56 +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.lsp.auth.model; - -public class LoginDetails { - private LoginType loginType; - private boolean isLoggedIn; - private String issuerUrl; - - public final void setLoginType(final LoginType loginType) { - this.loginType = loginType; - } - - public final LoginType getLoginType() { - return this.loginType; - } - - public final void setIsLoggedIn(final boolean isLoggedIn) { - this.isLoggedIn = isLoggedIn; - } - - public final boolean getIsLoggedIn() { - return this.isLoggedIn; - } - - public final void setIssuerUrl(final String issuerUrl) { - this.issuerUrl = issuerUrl; - } - - public final String getIssuerUrl() { - return this.issuerUrl; - } - - public final boolean equals(final LoginDetails loginDetails2) { - if (loginDetails2 == null) { - return false; - } - - LoginType loginType2 = loginDetails2.getLoginType(); - boolean isLoggedIn2 = loginDetails2.getIsLoggedIn(); - String issuerUrl2 = loginDetails2.getIssuerUrl(); - - if (loginType == null && loginType2 != null || loginType != null && loginType2 == null) { - return false; - } - - if (issuerUrl == null && issuerUrl2 != null || issuerUrl != null && issuerUrl2 == null) { - return false; - } - - return isLoggedIn == isLoggedIn2 - && (loginType == null && loginType2 == null || loginType.equals(loginType2)) - && (issuerUrl == null && issuerUrl2 == null || issuerUrl.equals(issuerUrl2)); - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java index 0a34e949f..c50d06384 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/connection/QLspConnectionProvider.java @@ -21,6 +21,7 @@ import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspManagerProvider; import software.aws.toolkits.eclipse.amazonq.telemetry.LanguageServerTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata; +import software.aws.toolkits.eclipse.amazonq.util.ArchitectureUtils; import software.aws.toolkits.eclipse.amazonq.util.ProxyUtil; import software.aws.toolkits.telemetry.TelemetryDefinitions.Result; @@ -60,6 +61,9 @@ protected final void addEnvironmentVariables(final Map env) { env.put("NODE_EXTRA_CA_CERTS", caCertPreference); env.put("AWS_CA_BUNDLE", caCertPreference); } + if (ArchitectureUtils.isWindowsArm()) { + env.put("DISABLE_INDEXING_LIBRARY", "true"); + } env.put("ENABLE_INLINE_COMPLETION", "true"); env.put("ENABLE_TOKEN_PROVIDER", "true"); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/encryption/DefaultLspEncryptionManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/encryption/DefaultLspEncryptionManager.java index f8ccf8fc1..8e3006563 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/encryption/DefaultLspEncryptionManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/encryption/DefaultLspEncryptionManager.java @@ -69,16 +69,16 @@ public void initializeEncryptedCommunication(final OutputStream serverStdin) { } } - public static class Builder { + public static final class Builder { private LspEncryptionKey lspEncryptionKey; - public final Builder withLspEncryptionKey(final LspEncryptionKey lspEncryptionKey) { + public Builder withLspEncryptionKey(final LspEncryptionKey lspEncryptionKey) { this.lspEncryptionKey = lspEncryptionKey; return this; } - public final DefaultLspEncryptionManager build() { + public DefaultLspEncryptionManager build() { return new DefaultLspEncryptionManager(this); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/DefaultLspManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/DefaultLspManager.java index 2821976c8..e7e16b807 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/DefaultLspManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/DefaultLspManager.java @@ -280,39 +280,39 @@ private static boolean remoteVersionIsGreater(final ArtifactVersion remote, fina return remote.compareTo(storedValue) > 0; } - public static class Builder { + public static final class Builder { private String manifestUrl; private Path workingDirectory; private String lspExecutablePrefix; private PluginPlatform platformOverride; private PluginArchitecture architectureOverride; - public final Builder withManifestUrl(final String manifestUrl) { + public Builder withManifestUrl(final String manifestUrl) { this.manifestUrl = manifestUrl; return this; } - public final Builder withDirectory(final Path workingDirectory) { + public Builder withDirectory(final Path workingDirectory) { this.workingDirectory = workingDirectory; return this; } - public final Builder withLspExecutablePrefix(final String lspExecutablePrefix) { + public Builder withLspExecutablePrefix(final String lspExecutablePrefix) { this.lspExecutablePrefix = lspExecutablePrefix; return this; } - public final Builder withPlatformOverride(final PluginPlatform platformOverride) { + public Builder withPlatformOverride(final PluginPlatform platformOverride) { this.platformOverride = platformOverride; return this; } - public final Builder withArchitectureOverride(final PluginArchitecture architectureOverride) { + public Builder withArchitectureOverride(final PluginArchitecture architectureOverride) { this.architectureOverride = architectureOverride; return this; } - public final DefaultLspManager build() { + public DefaultLspManager build() { return new DefaultLspManager(this); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspConstants.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspConstants.java index 3e9ef86fa..64c89746e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspConstants.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspConstants.java @@ -15,7 +15,7 @@ private LspConstants() { // Prevent instantiation } - public static final String CW_MANIFEST_URL = "https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json"; + public static final String CW_MANIFEST_URL = "https://d3akiidp1wvqyg.cloudfront.net/qAgenticChatServer/0/manifest.json"; public static final int MANIFEST_MAJOR_VERSION = 0; public static final String CW_LSP_FILENAME = "aws-lsp-codewhisperer.js"; @@ -26,13 +26,13 @@ private LspConstants() { public static final String LSP_SERVER_FOLDER = "servers"; public static final String LSP_SUBDIRECTORY = "lsp"; - public static final String AMAZONQ_LSP_SUBDIRECTORY = Paths.get(LSP_SUBDIRECTORY, "AmazonQ").toString(); + public static final String AMAZONQ_LSP_SUBDIRECTORY = Paths.get(LSP_SUBDIRECTORY, "AmazonQAgentic").toString(); public static final VersionRange LSP_SUPPORTED_VERSION_RANGE = createVersionRange(); private static VersionRange createVersionRange() { try { - return VersionRange.createFromVersionSpec("[3.1.2, 3.10.0)"); + return VersionRange.createFromVersionSpec("[1.0.0, 1.10.0)"); } catch (InvalidVersionSpecificationException e) { throw new AmazonQPluginException("Failed to parse LSP supported version range", e); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspInstallation.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspInstallation.java deleted file mode 100644 index b0f10c659..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/LspInstallation.java +++ /dev/null @@ -1,8 +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.lsp.manager; - -import java.nio.file.Path; - -public record LspInstallation(Path nodeExecutable, Path lspJs) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/FileSystemLspFetcher.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/FileSystemLspFetcher.java index af02aeac0..2c1612c30 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/FileSystemLspFetcher.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/FileSystemLspFetcher.java @@ -10,11 +10,11 @@ import software.aws.toolkits.eclipse.amazonq.util.PluginArchitecture; import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; -public class FileSystemLspFetcher { +public final class FileSystemLspFetcher { private final Path sourceFile; - public FileSystemLspFetcher(final Builder builder) { + private FileSystemLspFetcher(final Builder builder) { this.sourceFile = builder.sourceFile; } @@ -22,7 +22,7 @@ public static Builder builder() { return new Builder(); } - public final boolean fetch(final PluginPlatform platform, final PluginArchitecture architecture, final Path destination) { + public boolean fetch(final PluginPlatform platform, final PluginArchitecture architecture, final Path destination) { try { if (Files.isDirectory(sourceFile)) { ArtifactUtils.copyDirectory(sourceFile, destination); @@ -37,15 +37,15 @@ public final boolean fetch(final PluginPlatform platform, final PluginArchitectu return true; } - public static class Builder { + public static final class Builder { private Path sourceFile; - public final Builder withSourceFile(final Path sourceFile) { + public Builder withSourceFile(final Path sourceFile) { this.sourceFile = sourceFile; return this; } - public final FileSystemLspFetcher build() { + public FileSystemLspFetcher build() { return new FileSystemLspFetcher(this); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcher.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcher.java index 5a02a5f53..8d1faf8dc 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcher.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcher.java @@ -49,14 +49,12 @@ public final class RemoteLspFetcher implements LspFetcher { private final Manifest manifest; private final VersionRange versionRange; - private final boolean integrityChecking; private final HttpClient httpClient; private RecordLspSetupArgs args = new RecordLspSetupArgs(); private RemoteLspFetcher(final Builder builder) { this.manifest = builder.manifest; this.versionRange = builder.versionRange != null ? builder.versionRange : LspConstants.LSP_SUPPORTED_VERSION_RANGE; - this.integrityChecking = builder.integrityChecking != null ? builder.integrityChecking : true; this.httpClient = builder.httpClient != null ? builder.httpClient : HttpClientFactory.getInstance(); } @@ -420,7 +418,6 @@ private void deleteCachedVersion(final Path destinationFolder, final ArtifactVer public static class Builder { private Manifest manifest; private VersionRange versionRange; - private Boolean integrityChecking; private HttpClient httpClient; public final Builder withManifest(final Manifest manifest) { @@ -433,11 +430,6 @@ public final Builder withVersionRange(final VersionRange versionRange) { return this; } - public final Builder withIntegrityChecking(final boolean integrityChecking) { - this.integrityChecking = integrityChecking; - return this; - } - public final Builder withHttpClient(final HttpClient httpClient) { this.httpClient = httpClient; return this; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/BearerCredentials.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/BearerCredentials.java deleted file mode 100644 index 6357d969e..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/BearerCredentials.java +++ /dev/null @@ -1,17 +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.lsp.model; - -public class BearerCredentials { - - private String token; - - public final String getToken() { - return token; - } - - public final void setToken(final String token) { - this.token = token; - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ChatOptions.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ChatOptions.java index 55ceff087..04e378272 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ChatOptions.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ChatOptions.java @@ -3,4 +3,4 @@ package software.aws.toolkits.eclipse.amazonq.lsp.model; -public record ChatOptions(QuickActions quickActions) { } +public record ChatOptions(QuickActions quickActions, boolean history, boolean export) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommand.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommand.java index c0a072139..7af237d9b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommand.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommand.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.lsp.model; public record ContextCommand(String command, String description, String placeholder) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandGroup.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandGroup.java deleted file mode 100644 index bce9404ac..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandGroup.java +++ /dev/null @@ -1,5 +0,0 @@ -package software.aws.toolkits.eclipse.amazonq.lsp.model; - -import java.util.List; - -public record ContextCommandGroup(List commands) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandParams.java deleted file mode 100644 index 8cd09e930..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/ContextCommandParams.java +++ /dev/null @@ -1,5 +0,0 @@ -package software.aws.toolkits.eclipse.amazonq.lsp.model; - -import java.util.List; - -public record ContextCommandParams(List contextCommandGroups) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/GetConfigurationFromServerParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/GetConfigurationFromServerParams.java index e3bd43f3c..ddb0dbd72 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/GetConfigurationFromServerParams.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/GetConfigurationFromServerParams.java @@ -4,8 +4,25 @@ package software.aws.toolkits.eclipse.amazonq.lsp.model; public class GetConfigurationFromServerParams { + public enum ExpectedResponseType { + CUSTOMIZATION, Q_DEVELOPER_PROFILE, DEFAULT + } + private String section; + public GetConfigurationFromServerParams(final ExpectedResponseType responseType) { + switch (responseType) { + case CUSTOMIZATION: + section = "aws.q.customizations"; + break; + case Q_DEVELOPER_PROFILE: + section = "aws.q.developerProfiles"; + break; + default: + section = "aws.q"; + } + } + public final String getSection() { return this.section; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/LspServerConfigurations.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/LspServerConfigurations.java index 0e0c2a4e7..57ac25089 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/LspServerConfigurations.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/LspServerConfigurations.java @@ -5,17 +5,17 @@ import java.util.List; -import software.aws.toolkits.eclipse.amazonq.views.model.Customization; +import software.aws.toolkits.eclipse.amazonq.views.model.Configuration; -public class LspServerConfigurations { +public class LspServerConfigurations { - private final List customizations; + private final List configurations; - public LspServerConfigurations(final List customizations) { - this.customizations = customizations; + public LspServerConfigurations(final List configurations) { + this.configurations = configurations; } - public final List getCustomizations() { - return this.customizations; + public final List getConfigurations() { + return this.configurations; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayloadData.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenFileDiffParams.java similarity index 52% rename from plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayloadData.java rename to plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenFileDiffParams.java index 1f2d904c6..ea507d18c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayloadData.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenFileDiffParams.java @@ -3,9 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.lsp.model; -import com.fasterxml.jackson.annotation.JsonProperty; +import java.net.URI; -public record UpdateCredentialsPayloadData( - @JsonProperty("data") BearerCredentials data - ) { +public record OpenFileDiffParams(URI originalFileUri, String originalFileContent, Boolean isDeleted, + String fileContent) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenTabUiResponse.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenTabUiResponse.java new file mode 100644 index 000000000..f03a438dd --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/OpenTabUiResponse.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.lsp.model; + +public record OpenTabUiResponse(boolean success, Object result) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/TelemetryEvent.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/TelemetryEvent.java index 95c88cdad..f473f63c8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/TelemetryEvent.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/TelemetryEvent.java @@ -5,4 +5,8 @@ import java.util.Map; -public record TelemetryEvent(String name, String result, Map data, ErrorData errorData) { } +import com.fasterxml.jackson.annotation.JsonProperty; + +public record TelemetryEvent(@JsonProperty("name") String name, @JsonProperty("result") String result, + @JsonProperty("data") Map data, @JsonProperty("errorData") ErrorData errorData) { +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayload.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayload.java index 1af3be1b0..861e62dc7 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayload.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/model/UpdateCredentialsPayload.java @@ -7,6 +7,7 @@ public record UpdateCredentialsPayload( @JsonProperty("data") String data, + @JsonProperty("metadata") ConnectionMetadata metadata, @JsonProperty("encrypted") Boolean encrypted ) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java index 6ca0d8654..9fc1dbb60 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java @@ -7,12 +7,12 @@ import org.osgi.framework.BundleContext; import software.aws.toolkits.eclipse.amazonq.broker.EventBroker; -import software.aws.toolkits.eclipse.amazonq.chat.ChatStateManager; import software.aws.toolkits.eclipse.amazonq.configuration.DefaultPluginStore; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; import software.aws.toolkits.eclipse.amazonq.inlineChat.InlineChatEditorListener; import software.aws.toolkits.eclipse.amazonq.lsp.auth.DefaultLoginService; import software.aws.toolkits.eclipse.amazonq.lsp.auth.LoginService; +import software.aws.toolkits.eclipse.amazonq.providers.browser.AmazonQBrowserProvider; import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProviderImpl; import software.aws.toolkits.eclipse.amazonq.telemetry.service.DefaultTelemetryService; @@ -21,6 +21,7 @@ import software.aws.toolkits.eclipse.amazonq.util.DefaultCodeReferenceLoggingService; import software.aws.toolkits.eclipse.amazonq.util.LoggingService; import software.aws.toolkits.eclipse.amazonq.util.PluginLogger; +import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.views.router.ViewRouter; import software.aws.toolkits.eclipse.workspace.WorkspaceChangeListener; @@ -59,10 +60,11 @@ public Activator() { @Override public final void stop(final BundleContext context) throws Exception { - ChatStateManager.getInstance().dispose(); + AmazonQBrowserProvider.getInstance().dispose(); super.stop(context); plugin = null; workspaceListener.stop(); + ThreadingUtils.shutdown(); } public static Activator getDefault() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/preferences/AmazonQPreferencePage.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/preferences/AmazonQPreferencePage.java index 463c6968b..9d4c83d1c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/preferences/AmazonQPreferencePage.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/preferences/AmazonQPreferencePage.java @@ -4,9 +4,10 @@ package software.aws.toolkits.eclipse.amazonq.preferences; import org.eclipse.jface.preference.BooleanFieldEditor; -import org.eclipse.jface.preference.FileFieldEditor; import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.FileFieldEditor; import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.StringFieldEditor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; @@ -15,14 +16,17 @@ import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.dialogs.PreferencesUtil; -import software.aws.toolkits.eclipse.amazonq.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams.ExpectedResponseType; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.telemetry.AwsTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; @@ -95,8 +99,7 @@ protected final void createFieldEditors() { createHttpsProxyField(); createCaCertField(); - GetConfigurationFromServerParams params = new GetConfigurationFromServerParams(); - params.setSection("aws.q"); + GetConfigurationFromServerParams params = new GetConfigurationFromServerParams(ExpectedResponseType.DEFAULT); Activator.getLspProvider().getAmazonQServer().thenCompose(server -> server.getConfigurationFromServer(params)); } @@ -373,4 +376,16 @@ protected void adjustGridLayout() { // deliberately left blank to prevent multiple columns from implicitly being created } + public static void openPreferencePane() { + Display.getDefault().asyncExec(() -> { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( + Display.getDefault().getActiveShell(), + "software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage", + new String[] {"software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage"}, + null + ); + dialog.open(); + }); + } + } 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 84cabfa5b..7d216ba75 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 @@ -8,28 +8,17 @@ 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.broker.events.ChatWebViewAssetState; 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; -import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActions; -import software.aws.toolkits.eclipse.amazonq.lsp.model.QuickActionsCommandGroup; 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; @@ -94,22 +83,6 @@ public Object function(final Object[] arguments) { 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(content.get()); } @@ -120,6 +93,7 @@ private Optional resolveContent() { } String chatJsPath = chatAsset.get(); + String themeVariables = chatTheme.getThemeVariables(); return Optional.of(String.format(""" @@ -133,80 +107,32 @@ private Optional resolveContent() { img-src 'self' data:; object-src 'none'; base-uri 'none'; connect-src swt:;" > Amazon Q Chat - %s + %s - """, chatJsPath, chatJsPath, generateCss(), generateJS(chatJsPath))); - } - - private String generateCss() { - return """ - - """; + """, chatJsPath, chatJsPath, themeVariables, generateJS(chatJsPath))); } private String generateJS(final String jsEntrypoint) { - var chatQuickActionConfig = generateQuickActionConfig(); - var contextCommands = generateContextCommands(); var disclaimerAcknowledged = Activator.getPluginStore().get(PluginStoreKeys.CHAT_DISCLAIMER_ACKNOWLEDGED); + var pairProgrammingAcknowledged = Activator.getPluginStore().get(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED); return String.format(""" - """, jsEntrypoint, getWaitFunction(), chatQuickActionConfig, "true".equals(disclaimerAcknowledged), contextCommands, - getArrowKeyBlockingFunction(), getSelectAllAndCopySupportFunctions(), getPreventEmptyPopupFunction(), - getFocusOnChatPromptFunction()); + """, jsEntrypoint, getWaitFunction(), "true".equals(disclaimerAcknowledged), "true".equals(pairProgrammingAcknowledged), + getInputFunctions()); } - private String getArrowKeyBlockingFunction() { + @SuppressWarnings("MethodLength") + private String getInputFunctions() { return """ window.addEventListener('load', () => { - const textarea = document.querySelector('textarea.mynah-chat-prompt-input'); - if (textarea) { - textarea.addEventListener('keydown', (event) => { - const cursorPosition = textarea.selectionStart; - const hasText = textarea.value.length > 0; + const isMacOs = () => navigator.platform.toUpperCase().indexOf('MAC') >= 0; + + const cursorPositions = new WeakMap(); + const undoStacks = new WeakMap(); + const redoStacks = new WeakMap(); + + const getCursorPosition = (element) => { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(element); + preCaretRange.setEnd(range.endContainer, range.endOffset); + return preCaretRange.toString().length; + } + return cursorPositions.get(element) || 0; + }; + + const selectAllContent = (element) => { + const range = document.createRange(); + range.selectNodeContents(element); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + cursorPositions.set(element, element.innerText.length); + }; + + const updateCursorPosition = (element, newPosition) => { + const position = Math.max(0, Math.min(newPosition, element.innerText.length)); + cursorPositions.set(element, position); + }; + + const addInputListener = (element) => { + cursorPositions.set(element, 0); + undoStacks.set(element, []); + redoStacks.set(element, []); + let isUndoRedoAction = false; + + const saveState = () => { + if (!isUndoRedoAction) { + const currentState = { + text: element.innerText, + cursorPosition: getCursorPosition(element) + }; + const undoStack = undoStacks.get(element); + undoStack.push(currentState); + redoStacks.set(element, []); + if (undoStack.length > 100) { + undoStack.shift(); + } + } + }; + + const undo = () => { + const undoStack = undoStacks.get(element); + const redoStack = redoStacks.get(element); + if (undoStack.length > 1) { + isUndoRedoAction = true; + redoStack.push(undoStack.pop()); + const previousState = undoStack[undoStack.length - 1]; + element.innerText = previousState.text; + updateCursorPosition(element, previousState.cursorPosition); + isUndoRedoAction = false; + } + }; + + const redo = () => { + const redoStack = redoStacks.get(element); + if (redoStack.length > 0) { + isUndoRedoAction = true; + const redoState = redoStack.pop(); + element.innerText = redoState.text; + updateCursorPosition(element, redoState.cursorPosition); + undoStacks.get(element).push(redoState); + isUndoRedoAction = false; + } + }; + + const updateCursorAfterInput = () => { + setTimeout(() => { + const newPosition = getCursorPosition(element); + updateCursorPosition(element, newPosition); + saveState(); + }, 0); + }; + + saveState(); + + element.addEventListener('input', updateCursorAfterInput); + element.addEventListener('paste', updateCursorAfterInput); + + element.addEventListener('keydown', (event) => { + const cmdOrCtrl = isMacOs() ? event.metaKey : event.ctrlKey; + + if (cmdOrCtrl && event.key === 'a') { + selectAllContent(element); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (cmdOrCtrl && event.key === 'z') { + if (event.shiftKey) { + redo(); + } else { + undo(); + } + event.preventDefault(); + event.stopPropagation(); + return; + } + + const currentText = element.innerText.trim(); + const hasText = currentText.length > 0; + const textLength = currentText.length; + const cursorPosition = getCursorPosition(element); - // block arrow keys on empty text area switch (event.key) { case 'ArrowLeft': if (!hasText || cursorPosition === 0) { event.preventDefault(); event.stopPropagation(); + } else { + updateCursorPosition(element, cursorPosition - 1); } break; case 'ArrowRight': - if (!hasText || cursorPosition === textarea.value.length) { + if (!hasText || cursorPosition === textLength) { event.preventDefault(); event.stopPropagation(); + } else { + updateCursorPosition(element, cursorPosition + 1); } break; - } - }); - } - }); - """; - } - private String getSelectAllAndCopySupportFunctions() { - return """ - 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(); - } - }); - } - }); + case 'ArrowUp': + updateCursorPosition(element, 0); + break; - 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(); + case 'ArrowDown': + updateCursorPosition(element, textLength); + break; } - }); - } - }); - """; - } + }, true); - private String getPreventEmptyPopupFunction() { - String selector = ".mynah-button" + ".mynah-button-secondary.mynah-button-border" + ".fill-state-always" - + ".mynah-chat-item-followup-question-option" + ".mynah-ui-clickable-item"; + element.addEventListener('focus', () => { + const newPosition = getCursorPosition(element); + updateCursorPosition(element, newPosition); + }); + }; - return """ - const observer = new MutationObserver((mutations) => { - try { - const selector = '%s'; + document.querySelectorAll('div.mynah-chat-prompt-input').forEach(addInputListener); + const observer = new MutationObserver((mutations) => { 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 && node.matches(selector)) { - attachEventListeners(node); + if (node.nodeType === 1) { + if (node.matches('div.mynah-chat-prompt-input')) { + addInputListener(node); } - // Check for nested matches - if (node.querySelectorAll) { - const buttons = node.querySelectorAll(selector); // Missing selector parameter - buttons.forEach(attachEventListeners); - } - } + node.querySelectorAll('div.mynah-chat-prompt-input').forEach(addInputListener); + } }); }); - } catch (error) { - console.error('Error in mutation observer:', error); - } - }); - - function attachEventListeners(element) { - if (!element || element.dataset.hasListener) return; // Prevent duplicate listeners - - const handleMouseOver = function(event) { - const textSpan = this.querySelector('span.mynah-button-label'); - if (textSpan && textSpan.scrollWidth <= textSpan.offsetWidth) { - event.stopImmediatePropagation(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - element.addEventListener('mouseover', handleMouseOver, true); - element.dataset.hasListener = 'true'; - } + }); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true - }); - """.formatted(selector); - } - - private String getFocusOnChatPromptFunction() { - return """ - window.addEventListener('load', () => { - const chatContainer = document.querySelector('.mynah-chat-prompt'); - if (chatContainer) { - chatContainer.addEventListener('click', (event) => { - if (!event.target.closest('.mynah-chat-prompt-input')) { - keepFocusOnPrompt(); - } - }); - } + observer.observe(document.body, { + childList: true, + subtree: true + }); }); """; } - /* - * Generates javascript for chat options to be supplied to Chat UI defined here - * https://github.com/aws/language-servers/blob/ - * 785f8dee86e9f716fcfa29b2e27eb07a02387557/chat-client/src/client/chat.ts#L87 - */ - private String generateQuickActionConfig() { - return Optional.ofNullable(AwsServerCapabiltiesProvider.getInstance().getChatOptions()) - .map(ChatOptions::quickActions).map(QuickActions::quickActionsCommandGroups) - .map(this::serializeQuickActionCommands).orElse("[]"); - } - - private String generateContextCommands() { - return Optional.ofNullable(AwsServerCapabiltiesProvider.getInstance().getContextCommands()) - .map(this::serializeQuickActionCommands).orElse("[]"); - } - - private String serializeQuickActionCommands(final List quickActionCommands) { - try { - ObjectMapper mapper = ObjectMapperFactory.getInstance(); - return mapper.writeValueAsString(quickActionCommands); - } catch (Exception e) { - Activator.getLogger().warn("Error occurred when json serializing quick action commands", e); - return ""; - } - } - private void handleMessageFromUI(final Browser browser, final Object[] arguments) { try { commandParser.parseCommand(arguments) @@ -413,7 +366,8 @@ private void handleMessageFromUI(final Browser browser, final Object[] arguments } } - public Optional resolveJsPath() { + + private Optional resolveJsPath() { var chatUiDirectory = getChatUiDirectory(); if (!isValid(chatUiDirectory)) { 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 16fe3af3f..e83da68bf 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 @@ -32,8 +32,4 @@ 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/browser/AmazonQBrowserProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProvider.java index ef79a8453..08d673c65 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProvider.java @@ -3,29 +3,45 @@ package software.aws.toolkits.eclipse.amazonq.providers.browser; +import java.util.HashMap; +import java.util.Map; + import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; import software.aws.toolkits.eclipse.amazonq.broker.events.BrowserCompatibilityState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; -public class AmazonQBrowserProvider { +public final class AmazonQBrowserProvider { + private static AmazonQBrowserProvider instance; + private boolean hasWebViewDependency = false; private PluginPlatform pluginPlatform; - private Browser browser; + private Map browserById; + private Map compositeById; + + private AmazonQBrowserProvider(final Builder builder) { + this.pluginPlatform = builder.pluginPlatform; + browserById = new HashMap<>(); + compositeById = new HashMap<>(); + } - public AmazonQBrowserProvider() { - this(PluginUtils.getPlatform()); + public static Builder builder() { + return new Builder(); } - // Test constructor that accepts a platform - public AmazonQBrowserProvider(final PluginPlatform platform) { - this.pluginPlatform = platform; + + public static synchronized AmazonQBrowserProvider getInstance() { + if (instance == null) { + instance = AmazonQBrowserProvider.builder().build(); + } + return instance; } /* @@ -38,22 +54,27 @@ public AmazonQBrowserProvider(final PluginPlatform platform) { * * @return true if the browser is compatible, false otherwise */ - public final boolean checkWebViewCompatibility(final String browserType) { + public synchronized boolean checkWebViewCompatibility(final String browserType, + final boolean publishUnconditionally) { String expectedType = pluginPlatform == PluginPlatform.WINDOWS ? "edge" : "webkit"; - this.hasWebViewDependency = expectedType.equalsIgnoreCase(browserType); - if (!this.hasWebViewDependency) { + boolean hasWebViewDependency = expectedType.equalsIgnoreCase(browserType); + + if (!hasWebViewDependency) { Activator.getLogger() .info("Browser detected:" + browserType + " is not of expected type: " + expectedType); } - Activator.getEventBroker().post(BrowserCompatibilityState.class, - hasWebViewDependency ? BrowserCompatibilityState.COMPATIBLE - : BrowserCompatibilityState.DEPENDENCY_MISSING); + if (publishUnconditionally || this.hasWebViewDependency != hasWebViewDependency) { + Activator.getEventBroker().post(BrowserCompatibilityState.class, + hasWebViewDependency ? BrowserCompatibilityState.COMPATIBLE + : BrowserCompatibilityState.DEPENDENCY_MISSING); + } + this.hasWebViewDependency = hasWebViewDependency; return this.hasWebViewDependency; } - public final int getBrowserStyle() { + public synchronized int getBrowserStyle() { return pluginPlatform == PluginPlatform.WINDOWS ? SWT.EDGE : SWT.WEBKIT; } @@ -62,23 +83,77 @@ public final int getBrowserStyle() { * returns boolean representing whether a browser type compatible with webview rendering for the current platform is found * @param parent */ - public final boolean setupBrowser(final Composite parent) { + public synchronized Browser setupBrowser(final Composite parent, final String componentId, + final boolean publishUnconditionally) { var browser = new Browser(parent, getBrowserStyle()); GridData layoutData = new GridData(GridData.FILL_BOTH); browser.setLayoutData(layoutData); - checkWebViewCompatibility(browser.getBrowserType()); + checkWebViewCompatibility(browser.getBrowserType(), publishUnconditionally); + // only set the browser if compatible webview browser can be found for the // platform if (hasWebViewDependency()) { - this.browser = browser; + browserById.put(componentId, browser); + return browser; + } + return null; + } + + public synchronized Browser getBrowser(final String componentId) { + return browserById.get(componentId); + } + + private synchronized Composite getDummyParent(final String componentId) { + return compositeById.get(componentId); + } + + public synchronized Browser getAndAttachBrowser(final Composite parent, final String componentId) { + var browser = getBrowser(componentId); + + // if browser is null or disposed, return null + if (browser == null || browser.isDisposed()) { + return null; + } else if (browser.getParent() != parent) { + // Re-parent existing browser + browser.setParent(parent); + disposeDummyParent(componentId); + } + return browser; + } + + public synchronized void preserveBrowser(final String componentId) { + var browser = getBrowser(componentId); + var dummyParent = getDummyParent(componentId); + + if (browser != null && !browser.isDisposed()) { + if (dummyParent == null || dummyParent.isDisposed()) { + dummyParent = new Composite(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), SWT.NONE); + dummyParent.setVisible(false); + } + browser.setParent(dummyParent); + compositeById.put(componentId, dummyParent); } - return hasWebViewDependency(); } - public final Browser getBrowser() { - return this.browser; + private synchronized void disposeDummyParent(final String componentId) { + var dummyParent = compositeById.get(componentId); + + if (dummyParent != null && !dummyParent.isDisposed()) { + dummyParent.dispose(); + dummyParent = null; + } + } + + public synchronized void disposeBrowser(final String componentId) { + var browser = getBrowser(componentId); + + if (browser != null && !browser.isDisposed()) { + browser.dispose(); + browser = null; + } + disposeDummyParent(componentId); } /* @@ -86,15 +161,11 @@ public final Browser getBrowser() { * * @return true if the last check found a compatible WebView, false otherwise */ - public final boolean hasWebViewDependency() { + public synchronized boolean hasWebViewDependency() { return this.hasWebViewDependency; } - public final void updateBrowser(final Browser browser) { - this.browser = browser; - } - - public final void publishBrowserCompatibilityState() { + public synchronized void publishBrowserCompatibilityState() { Display.getDefault().asyncExec(() -> { Display display = Display.getDefault(); Shell shell = display.getActiveShell(); @@ -105,9 +176,35 @@ public final void publishBrowserCompatibilityState() { Composite parent = new Composite(shell, SWT.NONE); parent.setVisible(false); - setupBrowser(parent); + setupBrowser(parent, "initBrowser", true); parent.dispose(); }); } + public void dispose() { + browserById.forEach((key, value) -> { + disposeBrowser(key); + }); + + compositeById.forEach((key, value) -> { + disposeDummyParent(key); + }); + } + + public static final class Builder { + private PluginPlatform pluginPlatform; + + public Builder withPluginPlatform(final PluginPlatform pluginPlatform) { + this.pluginPlatform = pluginPlatform; + return this; + } + + public AmazonQBrowserProvider build() { + if (this.pluginPlatform == null) { + this.pluginPlatform = PluginUtils.getPlatform(); + } + return new AmazonQBrowserProvider(this); + } + } + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProvider.java index 1f218214f..1141bb180 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProvider.java @@ -4,12 +4,45 @@ package software.aws.toolkits.eclipse.amazonq.providers.lsp; import org.eclipse.lsp4j.services.LanguageServer; -import java.util.concurrent.CompletableFuture; + import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; +import java.util.concurrent.CompletableFuture; + +/** + * Provides management of Language Server Protocol (LSP) servers. + */ public interface LspProvider { + /** + * Sets a language server of the specified type. + * + * @param The type of language server + * @param lspType The class of the language server + * @param server The server instance + */ void setServer(Class lspType, T server); - void setAmazonQServer(LanguageServer server); + + /** + * Gets a language server of the specified type. + * + * @param The type of language server + * @param lspType The class of the language server + * @return A future that completes with the specified server + */ + CompletableFuture getServer(Class lspType); + + /** + * Activates a language server of the specified type. + * + * @param The type of language server + * @param lspType The class of the language server to activate + */ + void activate(Class lspType); + + /** + * Gets the Amazon Q language server. + * + * @return A future that completes with the Amazon Q server + */ CompletableFuture getAmazonQServer(); } - diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProviderImpl.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProviderImpl.java index 509556cfd..908a44349 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProviderImpl.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/providers/lsp/LspProviderImpl.java @@ -7,10 +7,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; - import org.eclipse.lsp4j.services.LanguageServer; - import software.aws.toolkits.eclipse.amazonq.broker.events.AmazonQLspState; +import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.RecordLspSetupArgs; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -19,15 +18,12 @@ public final class LspProviderImpl implements LspProvider { private static final LspProviderImpl INSTANCE = new LspProviderImpl(); - private static final long TIMEOUT_SECONDS = 60L; - private final Map, CompletableFuture> futures; - private final Map, LanguageServer> servers; + private final Map, ServerEntry> serverRegistry; private LspProviderImpl() { - this.futures = new ConcurrentHashMap<>(); - this.servers = new ConcurrentHashMap<>(); + this.serverRegistry = new ConcurrentHashMap<>(); } public static LspProvider getInstance() { @@ -37,48 +33,73 @@ public static LspProvider getInstance() { @Override public void setServer(final Class lspType, final T server) { synchronized (lspType) { - servers.put(lspType, server); - CompletableFuture future = futures.remove(lspType); - if (future != null) { - future.complete(server); - } + ServerEntry entry = serverRegistry.computeIfAbsent(lspType, k -> new ServerEntry()); + entry.setServer(server); } } @Override - public void setAmazonQServer(final LanguageServer server) { + public void activate(final Class lspType) { synchronized (AmazonQLspServer.class) { - servers.put(AmazonQLspServer.class, server); - CompletableFuture future = futures.remove(AmazonQLspServer.class); - if (future != null) { - future.complete(server); + ServerEntry entry = serverRegistry.get(AmazonQLspServer.class); + if (entry != null && entry.getFuture() != null) { + entry.getFuture().complete(serverRegistry.get(lspType).getServer()); } - emitInitializeMetric(); - Activator.getEventBroker().post(AmazonQLspState.class, AmazonQLspState.ACTIVE); + onServerActivation(); } } - @SuppressWarnings("unchecked") - private CompletableFuture getServer(final Class lspType) { - synchronized (lspType) { - T server = (T) servers.get(lspType); - if (server != null) { - return CompletableFuture.completedFuture(server); - } + @Override + public CompletableFuture getAmazonQServer() { + return getServer(AmazonQLspServer.class); + } - CompletableFuture future = futures.computeIfAbsent(lspType, k -> new CompletableFuture<>()); - return future.orTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) - .thenApply(lspServer -> (T) lspServer); + @Override + @SuppressWarnings("unchecked") + public CompletableFuture getServer(final Class lspType) { + ServerEntry entry = serverRegistry.computeIfAbsent(lspType, k -> new ServerEntry()); + if (entry.getServer() != null) { + return CompletableFuture.completedFuture((T) entry.getServer()); } + return entry.getFutureWithTimeout(TIMEOUT_SECONDS); } - @Override - public CompletableFuture getAmazonQServer() { - return getServer(AmazonQLspServer.class); + private void onServerActivation() { + emitInitializeMetric(); + Activator.getEventBroker().post(AmazonQLspState.class, AmazonQLspState.ACTIVE); + ChatCommunicationManager.getInstance(); } private void emitInitializeMetric() { LanguageServerTelemetryProvider.emitSetupInitialize(Result.SUCCEEDED, new RecordLspSetupArgs()); } + private static final class ServerEntry { + private LanguageServer server; + private CompletableFuture future; + + public void setServer(final LanguageServer server) { + this.server = server; + if (future != null) { + future.complete(server); + } + } + + public LanguageServer getServer() { + return server; + } + + public CompletableFuture getFuture() { + return future; + } + + @SuppressWarnings("unchecked") + public CompletableFuture getFutureWithTimeout(final long timeoutSeconds) { + if (future == null) { + future = new CompletableFuture<>(); + } + return future.orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .thenApply(server -> (T) server); + } + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/CodeWhispererTelemetryProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/CodeWhispererTelemetryProvider.java index 78804055d..7057757ac 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/CodeWhispererTelemetryProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/CodeWhispererTelemetryProvider.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.telemetry; import java.time.Instant; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/LanguageServerTelemetryProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/LanguageServerTelemetryProvider.java index b95006a60..ef2c5e2e5 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/LanguageServerTelemetryProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/LanguageServerTelemetryProvider.java @@ -77,7 +77,7 @@ public static void emitSetupInitialize(final Result result, final RecordLspSetup //final step completing makes call to complete full process emitSetupAll(result, args); } - public static void emitSetupAll(final Result result, final RecordLspSetupArgs args) { + private static void emitSetupAll(final Result result, final RecordLspSetupArgs args) { if (result == null || args == null) { return; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/ToolkitTelemetryProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/ToolkitTelemetryProvider.java index a8703dfb4..6724a6311 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/ToolkitTelemetryProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/ToolkitTelemetryProvider.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.telemetry; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/UiTelemetryProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/UiTelemetryProvider.java index 59e6534f6..bfe5de60a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/UiTelemetryProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/UiTelemetryProvider.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.telemetry; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/metadata/ExceptionMetadata.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/metadata/ExceptionMetadata.java index 847664cc7..9d15e64e8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/metadata/ExceptionMetadata.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/metadata/ExceptionMetadata.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.telemetry.metadata; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java index 0d813cab8..d6316893b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/telemetry/service/DefaultTelemetryService.java @@ -197,21 +197,6 @@ public static class Builder { private ToolkitTelemetryClient telemetryClient; private ClientMetadata clientMetadata; - public final Builder withTelemetryRegion(final Region region) { - this.region = region; - return this; - } - - public final Builder withTelemetryEndpoint(final String endpoint) { - this.endpoint = endpoint; - return this; - } - - public final Builder withIdentityPool(final String identityPool) { - this.identityPool = identityPool; - return this; - } - public final Builder withTelemetryClient(final ToolkitTelemetryClient telemetryClient) { this.telemetryClient = telemetryClient; return this; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ArchitectureUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ArchitectureUtils.java new file mode 100644 index 000000000..9d1f15453 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ArchitectureUtils.java @@ -0,0 +1,15 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +public final class ArchitectureUtils { + + private ArchitectureUtils() { } + + public static boolean isWindowsArm() { + String processorIdentifier = System.getenv("PROCESSOR_IDENTIFIER"); + return processorIdentifier != null && processorIdentifier.contains("ARM") && PluginUtils.getPlatform().equals(PluginPlatform.WINDOWS); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoCloseBracketConfig.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoCloseBracketConfig.java deleted file mode 100644 index 1a073b2fa..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoCloseBracketConfig.java +++ /dev/null @@ -1,9 +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.util; - -public record AutoCloseBracketConfig(boolean isParenAutoClosed, boolean isAngleBracketAutoClosed, - boolean isStringAutoClosed, boolean isBracesAutoClosed) { - -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerDocumentListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerDocumentListener.java index cf2fe59cd..f19f25c56 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerDocumentListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerDocumentListener.java @@ -3,19 +3,20 @@ package software.aws.toolkits.eclipse.amazonq.util; +import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextEditor; + +import java.util.concurrent.ExecutionException; + import org.eclipse.core.commands.IExecutionListener; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; import software.aws.toolkits.eclipse.amazonq.inlineChat.InlineChatSession; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; -import static software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils.getActiveTextEditor; - -import java.util.concurrent.ExecutionException; - public final class AutoTriggerDocumentListener implements IDocumentListener, IAutoTriggerListener { private static final String UNDO_COMMAND_ID = "org.eclipse.ui.edit.undo"; @@ -39,15 +40,18 @@ public synchronized void documentChanged(final DocumentEvent e) { if (!shouldSendQuery(e, qSes)) { return; } - if (!qSes.isActive()) { - var editor = getActiveTextEditor(); + var editor = getActiveTextEditor(); + + if (!qSes.isActive() && !(editor.getEditorInput() instanceof InMemoryInput)) { try { qSes.start(editor); } catch (ExecutionException e1) { return; } } - qSes.invoke(qSes.getViewer().getTextWidget().getCaretOffset(), e.getText().length()); + if (!(editor.getEditorInput() instanceof InMemoryInput)) { + qSes.invoke(qSes.getViewer().getTextWidget().getCaretOffset(), e.getText().length()); + } } private boolean shouldSendQuery(final DocumentEvent e, final QInvocationSession session) { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerTopLevelListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerTopLevelListener.java index 344dd9e71..6f20f3be2 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerTopLevelListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/AutoTriggerTopLevelListener.java @@ -21,10 +21,6 @@ public AutoTriggerTopLevelListener() { } - public AutoTriggerTopLevelListener(final T partListener) { - this.partListener = partListener; - } - public void addPartListener(final T partListener) { this.partListener = partListener; } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceAcceptanceCallback.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceAcceptanceCallback.java deleted file mode 100644 index 95ca2d909..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceAcceptanceCallback.java +++ /dev/null @@ -1,11 +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.util; - -import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionItem; - -@FunctionalInterface -public interface CodeReferenceAcceptanceCallback { - void onCallback(InlineCompletionItem suggestionItem, int startLine); -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggedProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggedProvider.java index 1d6c3160b..e70172f60 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggedProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggedProvider.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.util; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggingService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggingService.java index fe2b33b07..602afd01b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggingService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/CodeReferenceLoggingService.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.util; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java index cdbf606e8..e3726514c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/Constants.java @@ -26,7 +26,9 @@ private Constants() { public static final String LSP_CW_OPT_OUT_KEY = "shareCodeWhispererContentWithAWS"; public static final String LSP_CODE_REFERENCES_OPT_OUT_KEY = "includeSuggestionsWithCodeReferences"; public static final String IDE_CUSTOMIZATION_NOTIFICATION_TITLE = "Amazon Q Customization"; + public static final String IDE_DEVELOPER_PROFILES_NOTIFICATION_TITLE = "Amazon Q Developer Profile"; public static final String IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE = "Amazon Q inline suggestions are now coming from the %s"; + public static final String IDE_DEVELOPER_PROFILES_NOTIFICATION_BODY_TEMPLATE = "You're using the '%s' profile for Amazon Q."; public static final String MANIFEST_DEPRECATED_NOTIFICATION_KEY = "doNotShowDeprecatedManifest"; public static final String MANIFEST_DEPRECATED_NOTIFICATION_TITLE = "Update Amazon Q Extension"; public static final String MANIFEST_DEPRECATED_NOTIFICATION_BODY = "This version of the plugin" @@ -35,8 +37,6 @@ private Constants() { public static final String LOGIN_TYPE_KEY = "LOGIN_TYPE"; public static final String LOGIN_IDC_PARAMS_KEY = "IDC_PARAMS"; public static final String SSO_TOKEN_ID = "SSO_TOKEN_IN"; - public static final String PROXY_UPDATE_NOTIFICATION_TITLE = "Proxy settings changed"; - public static final String PROXY_UPDATE_NOTIFICATION_DESCRIPTION = "Proxy changes detected. Please restart the extension for it to take effect"; public static final String AWS_BUILDER_ID_URL = "https://view.awsapps.com/start"; public static final String IDC_PROFILE_NAME = "eclipse-q-profile"; public static final String IDC_SESSION_NAME = "eclipse-q-session"; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/DefaultCodeReferenceLoggingService.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/DefaultCodeReferenceLoggingService.java index d73ac20bc..ab0f72849 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/DefaultCodeReferenceLoggingService.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/DefaultCodeReferenceLoggingService.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.util; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/JsonHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/JsonHandler.java index b75d48952..b43037e7d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/JsonHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/JsonHandler.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -47,10 +48,44 @@ public T convertObject(final Object obj, final Class cls) { } public JsonNode getValueForKey(final Object obj, final String key) { - var paramsNode = objectMapper.valueToTree(obj); - if (paramsNode.has(key)) { - return paramsNode.get(key); + JsonNode currentNode = objectMapper.valueToTree(obj); + + String[] keyParts = key.split("\\."); + for (String keyPart : keyParts) { + if (currentNode == null || !currentNode.has(keyPart)) { + return null; + } + currentNode = currentNode.get(keyPart); } - return null; + + return currentNode; + } + + public JsonNode addValueForKey(final Object obj, final String key, final Object value) { + ObjectNode rootNode; + if (obj instanceof JsonNode) { + rootNode = (ObjectNode) obj; + } else { + rootNode = objectMapper.valueToTree(obj); + } + + String[] keyParts = key.split("\\."); + ObjectNode currentNode = rootNode; + + for (int i = 0; i < keyParts.length - 1; i++) { + String keyPart = keyParts[i]; + if (!currentNode.has(keyPart) || !currentNode.get(keyPart).isObject()) { + currentNode.putObject(keyPart); + } + currentNode = (ObjectNode) currentNode.get(keyPart); + } + + String finalKey = keyParts[keyParts.length - 1]; + if (value != null) { + JsonNode valueNode = objectMapper.valueToTree(value); + currentNode.set(finalKey, valueNode); + } + + return rootNode; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/LanguageUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/LanguageUtil.java index cc617815b..866f24753 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/LanguageUtil.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/LanguageUtil.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.util; import java.util.Set; @@ -14,7 +17,7 @@ private LanguageUtil() { "handlebars", "groovy", "go", "diff", "css", "c", "coffeescript", "clojure", "bibtex", "abap"); - public static String extractLanguageNameFromFileExtension( + private static String extractLanguageNameFromFileExtension( final String languageId) { if (languageId == null) { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java index c586e71a2..4543e168b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/PersistentToolkitNotification.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.util; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java index c1b777c10..5e419fe44 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ProxyUtil.java @@ -110,7 +110,7 @@ private static String determineProxyScheme(final Proxy.Type proxyType, final URI }; } - protected static String getHttpsProxyPreferenceUrl() throws MalformedURLException { + private static String getHttpsProxyPreferenceUrl() throws MalformedURLException { String prefValue = Activator.getDefault().getPreferenceStore() .getString(AmazonQPreferencePage.HTTPS_PROXY); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java index 6a0a82581..6b833fd2a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QEclipseEditorUtils.java @@ -23,6 +23,7 @@ import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension5; +import org.eclipse.jface.viewers.ISelection; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.swt.SWT; @@ -35,21 +36,19 @@ import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; - -import org.eclipse.jface.viewers.ISelection; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.FileStoreEditorInput; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.part.MultiPageEditorPart; import org.eclipse.ui.texteditor.ITextEditor; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.editor.InMemoryInput; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; public final class QEclipseEditorUtils { @@ -57,30 +56,25 @@ private QEclipseEditorUtils() { // Prevent instantiation } - public static IWorkbenchPage getActivePage() { + private static IWorkbenchPage getActivePage() { IWorkbenchWindow window = getActiveWindow(); return window == null ? null : window.getActivePage(); } - public static IWorkbenchWindow getActiveWindow() { + private static IWorkbenchWindow getActiveWindow() { return PlatformUI.getWorkbench().getActiveWorkbenchWindow(); } - public static IWorkbenchPart getActivePart() { - IWorkbenchPage page = getActivePage(); - return page == null ? null : page.getActivePart(); - } - public static ITextEditor getActiveTextEditor() { IWorkbenchPage activePage = getActivePage(); return activePage == null ? null : asTextEditor(activePage.getActiveEditor()); } - public static ISelection getSelection(final ITextEditor textEditor) { + private static ISelection getSelection(final ITextEditor textEditor) { return textEditor.getSelectionProvider().getSelection(); } - public static ITextEditor asTextEditor(final IEditorPart editorPart) { + private static ITextEditor asTextEditor(final IEditorPart editorPart) { if (editorPart instanceof ITextEditor) { return (ITextEditor) editorPart; } else { @@ -97,7 +91,7 @@ public static ITextEditor asTextEditor(final IEditorPart editorPart) { } } - public static ITextViewer asTextViewer(final IEditorPart editorPart) { + private static ITextViewer asTextViewer(final IEditorPart editorPart) { return editorPart != null ? editorPart.getAdapter(ITextViewer.class) : null; } @@ -121,6 +115,9 @@ public static Optional getOpenFileUri() { public static Optional getOpenFileUri(final IEditorInput editorInput) { try { + if (editorInput instanceof InMemoryInput) { + return Optional.empty(); + } var filePath = getOpenFilePath(editorInput); var fileUri = Paths.get(filePath).toUri().toString(); return Optional.of(fileUri); @@ -138,7 +135,7 @@ private static Optional getOpenFilePath() { return Optional.of(getOpenFilePath(editor.getEditorInput())); } - public static String getOpenFilePath(final IEditorInput editorInput) { + private static String getOpenFilePath(final IEditorInput editorInput) { if (editorInput instanceof FileStoreEditorInput fileStoreEditorInput) { return fileStoreEditorInput.getURI().getPath(); } else if (editorInput instanceof IFileEditorInput fileEditorInput) { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java index 28187e7c1..fdc7039d1 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInlineInputListener.java @@ -74,7 +74,6 @@ public void onNewSuggestion() { int curLineInDoc = widget.getLineAtOffset(invocationOffset); int lineIdx = invocationOffset - widget.getOffsetAtLine(curLineInDoc); String contentInLine = widget.getLine(curLineInDoc); - String delimiter = widget.getLineDelimiter(); if (!rightCtxBuf.isEmpty() && normalSegmentCount > 1) { try { int adjustedOffset = QEclipseEditorUtils.getOffsetInFullyExpandedDocument(session.getViewer(), diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java index 160da8fa6..bbe20a821 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QInvocationSession.java @@ -64,7 +64,6 @@ public final class QInvocationSession extends QResource { private CaretListener caretListener = null; private QInlineInputListener inputListener = null; private QInlineTerminationListener terminationListener = null; - private final int[] headOffsetAtLine = new int[500]; private final boolean isTabOnly = false; private Consumer unsetVerticalIndent; private final ConcurrentHashMap> unresolvedTasks = new ConcurrentHashMap<>(); @@ -285,7 +284,7 @@ private synchronized void queryAsync(final InlineCompletionParams params, final }); } catch (InterruptedException e) { Activator.getLogger().error("Inline completion interrupted", e); - } catch (ExecutionException e) { + } catch (Exception e) { Activator.getLogger().error("Error executing inline completion", e); } }); @@ -408,7 +407,7 @@ public boolean isDecisionMade() { return state == QInvocationSessionState.DECISION_MADE; } - public synchronized void transitionToPreviewingState() { + private synchronized void transitionToPreviewingState() { assert state == QInvocationSessionState.INVOKING; state = QInvocationSessionState.SUGGESTION_PREVIEWING; if (changeStatusToPreviewing != null) { @@ -416,7 +415,7 @@ public synchronized void transitionToPreviewingState() { } } - public void transitionToInvokingState() { + private void transitionToInvokingState() { assert state == QInvocationSessionState.INACTIVE; state = QInvocationSessionState.INVOKING; if (changeStatusToQuerying != null) { @@ -450,13 +449,6 @@ public void setCaretMovementReason(final CaretMovementReason reason) { this.caretMovementReason = reason; } - public void setHeadOffsetAtLine(final int lineNum, final int offSet) throws IllegalArgumentException { - if (lineNum >= headOffsetAtLine.length || lineNum < 0) { - throw new IllegalArgumentException("Problematic index given"); - } - headOffsetAtLine[lineNum] = offSet; - } - public Font getInlineTextFont() { return inlineTextFont; } @@ -481,13 +473,6 @@ public CaretMovementReason getCaretMovementReason() { return caretMovementReason; } - public int getHeadOffsetAtLine(final int lineNum) throws IllegalArgumentException { - if (lineNum >= headOffsetAtLine.length || lineNum < 0) { - throw new IllegalArgumentException("Problematic index given"); - } - return headOffsetAtLine[lineNum]; - } - public InlineCompletionItem getCurrentSuggestion() { if (suggestionsContext == null) { Activator.getLogger().warn("QSuggestion context is null"); @@ -548,7 +533,7 @@ public void executeCallbackForCodeReference() { Activator.getCodeReferenceLoggingService().log(codeReference); } - public void setVerticalIndent(final int line, final int height) { + void setVerticalIndent(final int line, final int height) { var widget = viewer.getTextWidget(); widget.setLineVerticalIndent(line, height); unsetVerticalIndent = (caretLine) -> { @@ -556,7 +541,7 @@ public void setVerticalIndent(final int line, final int height) { }; } - public void unsetVerticalIndent(final int caretLine) { + void unsetVerticalIndent(final int caretLine) { if (unsetVerticalIndent != null) { unsetVerticalIndent.accept(caretLine); unsetVerticalIndent = null; @@ -575,7 +560,7 @@ public int getOutstandingPadding() { return inputListener.getOutstandingPadding(); } - public void primeListeners() { + private void primeListeners() { inputListener.onNewSuggestion(); paintListener.onNewSuggestion(); markSuggestionAsSeen(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QResource.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QResource.java index c1ea23cbc..41d830847 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QResource.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QResource.java @@ -13,7 +13,7 @@ class QResource implements IDisposable { private final List children = new ArrayList<>(); private boolean isDisposed = false; - public void addChild(final QResource child) { + private void addChild(final QResource child) { if (isDisposed) { throw new IllegalStateException("Cannot add a child to a disposed resource"); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QSuggestionsContext.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QSuggestionsContext.java index 41aa5f277..0c7ef1218 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QSuggestionsContext.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/QSuggestionsContext.java @@ -4,15 +4,11 @@ package software.aws.toolkits.eclipse.amazonq.util; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import software.aws.toolkits.eclipse.amazonq.lsp.model.InlineCompletionStates; - public class QSuggestionsContext { private List details = new ArrayList<>(); private String sessionId; - private HashMap suggestionCompletionResults = new HashMap(); private long requestedAtEpoch; private int currentIndex = -1; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SsoSession.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SsoSession.java deleted file mode 100644 index 3500e6b17..000000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/SsoSession.java +++ /dev/null @@ -1,74 +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.util; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class SsoSession { - @JsonProperty("startUrl") - private String startUrl; - - @JsonProperty("region") - private String region; - - @JsonProperty("accessToken") - private String accessToken; - - @JsonProperty("refreshToken") - private String refreshToken; - - @JsonProperty("expiresAt") - private String expiresAt; - - @JsonProperty("createdAt") - private String createdAt; - - public final String getStartUrl() { - return startUrl; - } - - public final void setStartUrl(final String startUrl) { - this.startUrl = startUrl; - } - - public final String getRegion() { - return region; - } - - public final void setRegion(final String region) { - this.region = region; - } - - public final String getAccessToken() { - return accessToken; - } - - public final void setAccessToken(final String accessToken) { - this.accessToken = accessToken; - } - - public final String getRefreshToken() { - return refreshToken; - } - - public final void setRefreshToken(final String refreshToken) { - this.refreshToken = refreshToken; - } - - public final String getExpiresAt() { - return expiresAt; - } - - public final void setExpiresAt(final String expiresAt) { - this.expiresAt = expiresAt; - } - - public final String getCreatedAt() { - return createdAt; - } - - public final void setCreatedAt(final String createdAt) { - this.createdAt = createdAt; - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThemeDetector.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThemeDetector.java index 0c16b7fed..d5f8893ba 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThemeDetector.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThemeDetector.java @@ -42,7 +42,7 @@ private Optional isDarkThemeFromEclipsePreferences() { return Optional.ofNullable(isDarkTheme); } - public boolean themeUsingDarkColors() throws Exception { + private boolean themeUsingDarkColors() throws Exception { ITheme currentTheme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme(); Color backgroundColor = currentTheme.getColorRegistry().get(ACTIVE_TAB_BG_KEY); // Check if the background color is dark by examining its RGB values diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThreadingUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThreadingUtils.java index b39100250..8b1d989b2 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThreadingUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/ThreadingUtils.java @@ -6,10 +6,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public final class ThreadingUtils { private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors(); - private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(CORE_POOL_SIZE); + private static final ScheduledExecutorService THREAD_POOL = Executors.newScheduledThreadPool(CORE_POOL_SIZE); private ThreadingUtils() { // prevent instantiation @@ -27,6 +29,10 @@ public static Future executeAsyncTaskAndReturnFuture(final Runnable task) { return THREAD_POOL.submit(task); } + public static Future scheduleAsyncTaskWithDelay(final Runnable task, final long msDelay) { + return THREAD_POOL.schedule(task, msDelay, TimeUnit.MILLISECONDS); + } + public static void shutdown() { THREAD_POOL.shutdown(); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java index 979afeab6..00022a98f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/UpdateUtils.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.util; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java new file mode 100644 index 000000000..f6a6e1565 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/util/WorkspaceUtils.java @@ -0,0 +1,28 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.util; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; + +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; + +public final class WorkspaceUtils { + + private WorkspaceUtils() { } + + public static void refreshAllProjects() { + IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); + for (IProject project : projects) { + try { + project.refreshLocal(IResource.DEPTH_INFINITE, null); + } catch (CoreException e) { + Activator.getLogger().warn("Failed to refresh project(s): " + e.getMessage()); + } + } + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java index c76bb494e..bdc3d06b2 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java @@ -4,6 +4,7 @@ package software.aws.toolkits.eclipse.amazonq.views; import java.util.Arrays; +import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; @@ -18,29 +19,25 @@ import com.fasterxml.jackson.databind.JsonNode; +import software.aws.toolkits.eclipse.amazonq.chat.ChatAsyncResultManager; import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; -import software.aws.toolkits.eclipse.amazonq.chat.models.CopyToClipboardParams; +import software.aws.toolkits.eclipse.amazonq.chat.ChatMessage; import software.aws.toolkits.eclipse.amazonq.chat.models.CursorState; -import software.aws.toolkits.eclipse.amazonq.chat.models.InfoLinkClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.InsertToCursorPositionParams; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthFollowUpClickedParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthFollowUpType; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; import software.aws.toolkits.eclipse.amazonq.util.Constants; -import software.aws.toolkits.eclipse.amazonq.util.JsonHandler; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; import software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils; import software.aws.toolkits.eclipse.amazonq.views.model.Command; import software.aws.toolkits.eclipse.amazonq.views.model.ParsedCommand; public class AmazonQChatViewActionHandler implements ViewActionHandler { - private final JsonHandler jsonHandler; private ChatCommunicationManager chatCommunicationManager; public AmazonQChatViewActionHandler(final ChatCommunicationManager chatCommunicationManager) { - this.jsonHandler = new JsonHandler(); this.chatCommunicationManager = chatCommunicationManager; } @@ -50,76 +47,87 @@ public AmazonQChatViewActionHandler(final ChatCommunicationManager chatCommunica @Override public final void handleCommand(final ParsedCommand parsedCommand, final Browser browser) { Command command = parsedCommand.getCommand(); - Object params = parsedCommand.getParams(); + ChatMessage message = new ChatMessage(parsedCommand.getParams()); switch (command) { case CHAT_SEND_PROMPT: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; + case CHAT_PROMPT_OPTION_CHANGE: case CHAT_QUICK_ACTION: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; - case CHAT_INFO_LINK_CLICK: - case CHAT_LINK_CLICK: - case CHAT_SOURCE_LINK_CLICK: - InfoLinkClickParams infoLinkClickParams = jsonHandler.convertObject(params, InfoLinkClickParams.class); - var link = infoLinkClickParams.getLink(); - if (link == null || link.isEmpty()) { - throw new IllegalArgumentException("Link parameter cannot be null or empty"); - } - PluginUtils.handleExternalLinkClick(link); - break; + case FILE_CLICK: case CHAT_READY: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; case CHAT_TAB_ADD: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; case CHAT_TAB_REMOVE: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; case CHAT_TAB_CHANGE: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; case CHAT_END_CHAT: - chatCommunicationManager.sendMessageToChatServer(command, params); + case CHAT_FEEDBACK: + case CHAT_FOLLOW_UP_CLICK: + case LIST_CONVERSATIONS: + case CONVERSATION_CLICK: + case CREATE_PROMPT: + case STOP_CHAT_RESPONSE: + case BUTTON_CLICK: + case TAB_BAR_ACTION: + chatCommunicationManager.sendMessageToChatServer(command, message); + break; + case CHAT_INFO_LINK_CLICK: + case CHAT_LINK_CLICK: + case CHAT_SOURCE_LINK_CLICK: + validateAndHandleLink(message.getValueAsString("link")); + chatCommunicationManager.sendMessageToChatServer(command, message); break; case CHAT_INSERT_TO_CURSOR_POSITION: - var insertToCursorParams = jsonHandler.convertObject(params, InsertToCursorPositionParams.class); - var cursorState = insertAtCursor(insertToCursorParams); + var cursorState = insertAtCursor(message); // add information about editor state and send telemetry event // only include files that are accessible via lsp which have absolute paths - // When this fails, we will still send the request for amazonq_interactWithMessage telemetry + // When this fails, we will still send the request for + // amazonq_interactWithMessage telemetry getOpenFileUri().ifPresent(filePathUri -> { - insertToCursorParams.setTextDocument(new TextDocumentIdentifier(filePathUri)); - cursorState.ifPresent(state -> insertToCursorParams.setCursorState(Arrays.asList(state))); + message.addValueForKey("textDocument", new TextDocumentIdentifier(filePathUri)); + cursorState.ifPresent(state -> message.addValueForKey("cursorState", Arrays.asList(state))); }); - chatCommunicationManager.sendMessageToChatServer(Command.TELEMETRY_EVENT, insertToCursorParams); - break; - case CHAT_FEEDBACK: - chatCommunicationManager.sendMessageToChatServer(command, params); - break; - case CHAT_FOLLOW_UP_CLICK: - chatCommunicationManager.sendMessageToChatServer(command, params); + chatCommunicationManager.sendMessageToChatServer(command, message); break; case TELEMETRY_EVENT: - // telemetry notification for insert to cursor is modified and forwarded to server in the InsertToCursorPosition handler - if (isInsertToCursorEvent(params)) { + // telemetry notification for insert to cursor is modified and forwarded to + // server in the InsertToCursorPosition handler + if (isInsertToCursorEvent(message)) { break; } - chatCommunicationManager.sendMessageToChatServer(command, params); + chatCommunicationManager.sendMessageToChatServer(command, message); break; case CHAT_COPY_TO_CLIPBOARD: - CopyToClipboardParams copyToClipboardParams = jsonHandler.convertObject(params, CopyToClipboardParams.class); - handleCopyToClipboard(copyToClipboardParams.code()); + handleCopyToClipboard(message.getValueAsString("code")); break; case AUTH_FOLLOW_UP_CLICKED: - AuthFollowUpClickedParams authFollowUpClickedParams = jsonHandler.convertObject(params, AuthFollowUpClickedParams.class); - handleAuthFollowUpClicked(authFollowUpClickedParams); + handleAuthFollowUpClicked(message); break; case DISCLAIMER_ACKNOWLEDGED: Activator.getPluginStore().put(PluginStoreKeys.CHAT_DISCLAIMER_ACKNOWLEDGED, "true"); break; + case PROMPT_OPTION_ACKNOWLEDGED: + if (!(message.getData() instanceof Map)) { + break; + } + + @SuppressWarnings("unchecked") + Map options = (Map) message.getData(); + String messageId = options.get("messageId"); + + if ("programmerModeCardId".equals(messageId)) { + Activator.getPluginStore().put(PluginStoreKeys.PAIR_PROGRAMMING_ACKNOWLEDGED, "true"); + } + break; + case GET_SERIALIZED_CHAT: + ChatAsyncResultManager.getInstance().setResult(parsedCommand.getRequestId(), message.getData()); + Activator.getLogger().info("Got serialized chat response for request ID: " + parsedCommand.getRequestId()); + break; + case CHAT_OPEN_TAB: + ChatAsyncResultManager.getInstance().setResult(parsedCommand.getRequestId(), message.getData()); + Activator.getLogger().info("Got open tab response for request ID: " + parsedCommand.getRequestId()); + break; + case OPEN_SETTINGS: + AmazonQPreferencePage.openPreferencePane(); + break; default: throw new AmazonQPluginException("Unexpected command received from Amazon Q Chat: " + command.toString()); } @@ -129,19 +137,26 @@ public final void handleCommand(final ParsedCommand parsedCommand, final Browser * Inserts the text present in parameters at caret position in editor * and returns cursor state range from the start caret to end caret, which includes the entire inserted text range */ - private Optional insertAtCursor(final InsertToCursorPositionParams insertToCursorParams) { + private Optional insertAtCursor(final ChatMessage message) { AtomicReference> range = new AtomicReference>(); Display.getDefault().syncExec(new Runnable() { @Override public void run() { - range.set(QEclipseEditorUtils.insertAtCursor(insertToCursorParams.getCode())); + range.set(QEclipseEditorUtils.insertAtCursor(message.getValueAsString("code"))); } }); return range.get().map(CursorState::new); } - private boolean isInsertToCursorEvent(final Object params) { - return Optional.ofNullable(jsonHandler.getValueForKey(params, "name")) + private void validateAndHandleLink(final String link) { + if (link == null || link.isEmpty()) { + throw new IllegalArgumentException("Link parameter cannot be null or empty"); + } + PluginUtils.handleExternalLinkClick(link); + } + + private boolean isInsertToCursorEvent(final ChatMessage message) { + return Optional.ofNullable(message.getValueForKey("name")) .map(JsonNode::asText) .map("insertToCursorPosition"::equals) .orElse(false); @@ -174,8 +189,8 @@ private void handleCopyToClipboard(final String selection) { }); } - private void handleAuthFollowUpClicked(final AuthFollowUpClickedParams params) { - String incomingType = params.authFollowupType(); + private void handleAuthFollowUpClicked(final ChatMessage message) { + String incomingType = message.getValueAsString("authFollowupType"); String fullAuth = AuthFollowUpType.FULL_AUTH.getValue(); String reAuth = AuthFollowUpType.RE_AUTH.getValue(); String missingScopes = AuthFollowUpType.MISSING_SCOPES.getValue(); 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 035ec89a4..e3619bc57 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.util.concurrent.Future; + import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.ProgressAdapter; import org.eclipse.swt.browser.ProgressEvent; @@ -10,7 +12,6 @@ import org.eclipse.swt.widgets.Display; import software.aws.toolkits.eclipse.amazonq.chat.ChatCommunicationManager; -import software.aws.toolkits.eclipse.amazonq.chat.ChatStateManager; import software.aws.toolkits.eclipse.amazonq.providers.assets.ChatWebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.providers.assets.WebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQViewCommonActions; @@ -20,15 +21,13 @@ public class AmazonQChatWebview extends AmazonQView implements ChatUiRequestList public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.AmazonQChatWebview"; private AmazonQViewCommonActions amazonQCommonActions; - private final ChatStateManager chatStateManager; private final ChatCommunicationManager chatCommunicationManager; private Browser browser; - private volatile boolean canDisposeState = false; private WebViewAssetProvider webViewAssetProvider; + private Future refreshFuture; public AmazonQChatWebview() { super(); - chatStateManager = ChatStateManager.getInstance(); chatCommunicationManager = ChatCommunicationManager.getInstance(); webViewAssetProvider = new ChatWebViewAssetProvider(); webViewAssetProvider.initialize(); @@ -37,20 +36,17 @@ public AmazonQChatWebview() { @Override public final Composite setupView(final Composite parent) { setupParentBackground(parent); - browser = chatStateManager.getBrowser(parent); + browser = getAndAttachBrowser(parent); // attempt to use existing browser with chat history if present, else create a // new one if (browser == null || browser.isDisposed()) { - canDisposeState = false; - var result = setupBrowser(parent); + browser = setupBrowser(parent); // if setup of amazon q view fails due to missing webview dependency, switch to // that view and don't setup rest of the content - if (!result) { + if (browser == null) { return parent; } - browser = getAndUpdateStateManager(); - browser.setVisible(false); browser.addProgressListener(new ProgressAdapter() { @Override @@ -58,54 +54,38 @@ public void completed(final ProgressEvent event) { Display.getDefault().asyncExec(() -> { if (!browser.isDisposed()) { browser.setVisible(true); + chatCommunicationManager.activate(); } }); } }); webViewAssetProvider.injectAssets(browser); - } else { - updateBrowser(browser); } super.setupView(parent); - parent.addDisposeListener(e -> chatStateManager.preserveBrowser()); + parent.addDisposeListener(e -> this.preserveBrowser()); amazonQCommonActions = getAmazonQCommonActions(); - chatCommunicationManager.setChatUiRequestListener(this); - - addFocusListener(parent, browser); setupAmazonQCommonActions(); return parent; } - private Browser getAndUpdateStateManager() { - var browser = getBrowser(); - chatStateManager.updateBrowser(browser); - return browser; - } - @Override public final void onSendToChatUi(final String message) { String script = "window.postMessage(" + message + ");"; - browser.getDisplay().asyncExec(() -> { - browser.evaluate(script); + Display.getDefault().asyncExec(() -> { + browser.execute(script); }); } - public final void disposeBrowserState() { - canDisposeState = true; - } - @Override public final void dispose() { chatCommunicationManager.removeListener(this); - if (canDisposeState) { - ChatStateManager.getInstance().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 973a10a25..d8d4f49c3 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java @@ -3,6 +3,8 @@ package software.aws.toolkits.eclipse.amazonq.views; +import java.util.UUID; + import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.events.FocusEvent; @@ -10,7 +12,6 @@ import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IViewSite; import software.aws.toolkits.eclipse.amazonq.providers.browser.AmazonQBrowserProvider; import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; @@ -19,15 +20,18 @@ public abstract class AmazonQView extends BaseAmazonQView { private AmazonQBrowserProvider browserProvider; private static final ThemeDetector THEME_DETECTOR = new ThemeDetector(); - - private IViewSite viewSite; + private final String componentId = UUID.randomUUID().toString(); protected AmazonQView() { - this.browserProvider = new AmazonQBrowserProvider(); + this.browserProvider = AmazonQBrowserProvider.getInstance(); } - public final Browser getBrowser() { - return browserProvider.getBrowser(); + final Browser getBrowser() { + return browserProvider.getBrowser(componentId); + } + + public final Browser getAndAttachBrowser(final Composite parent) { + return browserProvider.getAndAttachBrowser(parent, componentId); } protected final void setupParentBackground(final Composite parent) { @@ -37,12 +41,17 @@ protected final void setupParentBackground(final Composite parent) { parent.setBackground(bg); } - protected final boolean setupBrowser(final Composite parent) { - return browserProvider.setupBrowser(parent); + protected final Browser setupBrowser(final Composite parent) { + return browserProvider.setupBrowser(parent, componentId, false); } - protected final void updateBrowser(final Browser browser) { - browserProvider.updateBrowser(browser); + + protected final void preserveBrowser() { + browserProvider.preserveBrowser(componentId); + } + + public final void disposeBrowser() { + browserProvider.disposeBrowser(componentId); } /** @@ -63,17 +72,13 @@ public Composite setupView(final Composite parent) { Browser browser = getBrowser(); if (browser != null && !browser.isDisposed()) { - setupBrowserBackground(parent); + var bgColor = parent.getBackground(); + browser.setBackground(bgColor); } return parent; } - private void setupBrowserBackground(final Composite parent) { - var bgColor = parent.getBackground(); - getBrowser().setBackground(bgColor); - } - public final void addFocusListener(final Composite parent, final Browser browser) { parent.addFocusListener(new FocusListener() { @Override diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQViewContainer.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQViewContainer.java index 2dd4596ba..4b5e96bb9 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQViewContainer.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQViewContainer.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.views; @@ -22,27 +22,26 @@ public final class AmazonQViewContainer extends ViewPart implements EventObserver { public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.AmazonQViewContainer"; + private static final Map VIEWS; private Composite parentComposite; private volatile StackLayout layout; - private Map views; private volatile AmazonQViewType activeViewType; private volatile BaseAmazonQView currentView; private final ReentrantLock containerLock; - public AmazonQViewContainer() { - activeViewType = AmazonQViewType.CHAT_VIEW; - containerLock = new ReentrantLock(true); - - views = Map.of( - AmazonQViewType.CHAT_ASSET_MISSING_VIEW, new ChatAssetMissingView(), + static { + VIEWS = Map.of(AmazonQViewType.CHAT_ASSET_MISSING_VIEW, new ChatAssetMissingView(), AmazonQViewType.DEPENDENCY_MISSING_VIEW, new DependencyMissingView(), AmazonQViewType.RE_AUTHENTICATE_VIEW, new ReauthenticateView(), AmazonQViewType.LSP_STARTUP_FAILED_VIEW, new LspStartUpFailedView(), AmazonQViewType.CHAT_VIEW, new AmazonQChatWebview(), - AmazonQViewType.TOOLKIT_LOGIN_VIEW, new ToolkitLoginWebview() - ); + AmazonQViewType.TOOLKIT_LOGIN_VIEW, new ToolkitLoginWebview()); + } + public AmazonQViewContainer() { + activeViewType = AmazonQViewType.CHAT_VIEW; + containerLock = new ReentrantLock(true); Activator.getEventBroker().subscribe(AmazonQViewType.class, this); } @@ -65,11 +64,11 @@ private void updateChildView() { Display.getDefault().asyncExec(() -> { try { containerLock.lock(); - BaseAmazonQView newView = views.get(activeViewType); + BaseAmazonQView newView = VIEWS.get(activeViewType); if (currentView != null) { - if (currentView instanceof AmazonQChatWebview) { - ((AmazonQChatWebview) currentView).disposeBrowserState(); + if (currentView instanceof AmazonQView) { + ((AmazonQView) currentView).disposeBrowser(); } Control[] children = parentComposite.getChildren(); for (Control child : children) { @@ -99,17 +98,17 @@ private void updateChildView() { @Override public void onEvent(final AmazonQViewType newViewType) { - if (newViewType.equals(activeViewType) || !views.containsKey(newViewType)) { - return; - } + if (!VIEWS.containsKey(newViewType)) { + return; + } - containerLock.lock(); - activeViewType = newViewType; - containerLock.unlock(); + containerLock.lock(); + activeViewType = newViewType; + containerLock.unlock(); - if (parentComposite != null && !parentComposite.isDisposed()) { - updateChildView(); - } + if (parentComposite != null && !parentComposite.isDisposed()) { + updateChildView(); + } } @Override @@ -125,4 +124,5 @@ public void dispose() { super.dispose(); } + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/BaseAmazonQView.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/BaseAmazonQView.java index c5266cc4c..7c5cdfbc9 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/BaseAmazonQView.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/BaseAmazonQView.java @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.views; @@ -55,10 +55,6 @@ protected final AmazonQViewCommonActions getAmazonQCommonActions() { return amazonQCommonActions; } - protected final AmazonQStaticActions getAmazonQStaticActions() { - return amazonQStaticActions; - } - protected final Image loadImage(final String imagePath) { Image loadedImage = null; try { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ChangeProfileDialog.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ChangeProfileDialog.java new file mode 100644 index 000000000..315f1d73c --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ChangeProfileDialog.java @@ -0,0 +1,424 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil; +import software.aws.toolkits.eclipse.amazonq.util.PluginPlatform; +import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; +import software.aws.toolkits.eclipse.amazonq.views.model.QDeveloperProfile; + +public final class ChangeProfileDialog extends Dialog { + + private static final String WINDOW_TITLE = "Amazon Q Developer Profile"; + private static final String HEADER = "Change your Q Developer Profile"; + private static final String DESCRIPTION = "Choose the profile that meets your current working needs. When you change profiles, " + + "you will no longer have access to your current customizations, chats, code reviews, or any other " + + "code or content being generated by Amazon Q."; + + private Composite container; + private Font titleFont; + private Font descriptionFont; + private RadioButtonWithDescriptor selectedRadioButton; + private Font loadingLabelFont; + private Font scrollableLabelFont; + + public final class RadioButtonWithDescriptor extends Composite { + + private Button radioButton; + private StyledText profileNameAndRegionText; + private Label accountIdLabel; + private Font profileNameFont; + private Font accountIdFont; + private Font regionFont; + + public RadioButtonWithDescriptor(final Composite parent, final String profileName, final String region, + final String accountId, final int style) { + super(parent, SWT.NONE); + + GridLayout layout = new GridLayout(1, false); + layout.marginWidth = 0; + layout.marginHeight = 0; + layout.verticalSpacing = 2; + this.setLayout(layout); + + Composite topRow = new Composite(this, SWT.NONE); + GridLayout topRowLayout = new GridLayout(2, false); + topRowLayout.marginWidth = 0; + topRowLayout.marginHeight = 0; + topRow.setLayout(topRowLayout); + topRow.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + radioButton = new Button(topRow, SWT.RADIO | style); + radioButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + + profileNameFont = createFont(12, SWT.NORMAL); + regionFont = createFont(12, SWT.ITALIC); + + profileNameAndRegionText = new StyledText(topRow, SWT.READ_ONLY); + profileNameAndRegionText.setText(profileName + " - " + region); + + StyleRange profileStyle = new StyleRange(); + profileStyle.start = 0; + profileStyle.length = profileName.length() + 2; + profileStyle.font = profileNameFont; + + StyleRange regionStyle = new StyleRange(); + regionStyle.start = profileName.length() + 2; + regionStyle.length = region.length(); + regionStyle.font = regionFont; + + profileNameAndRegionText.setStyleRanges(new StyleRange[] {profileStyle, regionStyle}); + + GridData combinedData = new GridData(SWT.FILL, SWT.CENTER, true, false); + combinedData.horizontalIndent = PluginUtils.getPlatform().equals(PluginPlatform.WINDOWS) ? 3 : 0; + profileNameAndRegionText.setLayoutData(combinedData); + + profileNameAndRegionText.setBackground(topRow.getBackground()); + profileNameAndRegionText.setEditable(false); + profileNameAndRegionText.setCaret(null); + profileNameAndRegionText.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND)); + + accountIdFont = createFont(10, SWT.NORMAL); + accountIdLabel = new Label(this, SWT.WRAP); + accountIdLabel.setText("Account ID: " + accountId); + accountIdLabel.setFont(accountIdFont); + accountIdLabel.setForeground(getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); + accountIdLabel.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND)); + GridData accountIdData = new GridData(SWT.FILL, SWT.CENTER, true, false); + accountIdData.horizontalIndent = PluginUtils.getPlatform().equals(PluginPlatform.WINDOWS) ? 21 : 23; + accountIdLabel.setLayoutData(accountIdData); + + addDisposeListener(e -> { + if (profileNameFont != null && !profileNameFont.isDisposed()) { + profileNameFont.dispose(); + profileNameFont = null; + } + + if (accountIdFont != null && !accountIdFont.isDisposed()) { + accountIdFont.dispose(); + accountIdFont = null; + } + + if (regionFont != null && !regionFont.isDisposed()) { + regionFont.dispose(); + regionFont = null; + } + + if (radioButton != null && !radioButton.isDisposed()) { + radioButton.dispose(); + radioButton = null; + } + + if (profileNameAndRegionText != null && !profileNameAndRegionText.isDisposed()) { + profileNameAndRegionText.dispose(); + profileNameAndRegionText = null; + } + + if (accountIdLabel != null && !accountIdLabel.isDisposed()) { + accountIdLabel.dispose(); + accountIdLabel = null; + } + }); + } + + public void setSelection(final boolean isSelected) { + radioButton.setSelection(isSelected); + } + + public void addSelectionListener(final Runnable runnable) { + radioButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(final SelectionEvent event) { + runnable.run(); + } + }); + + profileNameAndRegionText.addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(final MouseEvent event) { + radioButton.setSelection(true); + runnable.run(); + } + }); + + accountIdLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(final MouseEvent event) { + radioButton.setSelection(true); + runnable.run(); + } + }); + } + } + + public ChangeProfileDialog(final Shell parentShell) { + super(parentShell); + } + + private Font createFont(final int size, final int style) { + FontData[] fontData = getShell().getDisplay().getSystemFont().getFontData(); + FontData newFontData = new FontData(fontData[0].getName(), size, // specify exact font size + style); // SWT.NORMAL, SWT.BOLD, SWT.ITALIC, or SWT.BOLD | SWT.ITALIC + return new Font(getShell().getDisplay(), newFontData); + } + + @Override + protected Control createDialogArea(final Composite parent) { + container = (Composite) super.createDialogArea(parent); + + GridLayout mainLayout = new GridLayout(1, false); + mainLayout.marginWidth = 15; + mainLayout.marginHeight = 15; + mainLayout.verticalSpacing = 10; + container.setLayout(mainLayout); + + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + gridData.widthHint = 450; + gridData.heightHint = 238; + container.setLayoutData(gridData); + + setupHeaderText(container); + + Composite stackComposite = new Composite(container, SWT.NONE); + stackComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + StackLayout stackLayout = new StackLayout(); + stackComposite.setLayout(stackLayout); + + Composite loadingComposite = setupLoadingComposite(stackComposite); + + ScrolledComposite scrolledComposite = new ScrolledComposite(stackComposite, + SWT.V_SCROLL | SWT.H_SCROLL); + scrolledComposite.setExpandHorizontal(true); + scrolledComposite.setExpandVertical(true); + + Composite radioButtonComposite = new Composite(scrolledComposite, SWT.NONE); + GridLayout radioLayout = new GridLayout(1, false); + radioLayout.marginWidth = 5; + radioLayout.marginHeight = 5; + radioLayout.verticalSpacing = 5; + radioButtonComposite.setLayout(radioLayout); + scrolledComposite.setContent(radioButtonComposite); + + stackLayout.topControl = loadingComposite; + stackComposite.layout(true, true); + + Label scrollableLabel = new Label(container, SWT.NONE); + GridData scrollableLabelData = new GridData(SWT.CENTER, SWT.CENTER, false, false); + scrollableLabelData.verticalIndent = 0; + scrollableLabel.setLayoutData(scrollableLabelData); + + scrollableLabelFont = createFont(16, SWT.BOLD); + scrollableLabel.setFont(scrollableLabelFont); + + Runnable showDownArrowWhenScrollable = new Runnable() { + @Override + public void run() { + int scrollPosition = scrolledComposite.getVerticalBar().getSelection(); + int maxScroll = scrolledComposite.getVerticalBar().getMaximum(); + int thumbSize = scrolledComposite.getVerticalBar().getThumb(); + + boolean isAtBottom = (scrollPosition + thumbSize) >= maxScroll; + + if (isAtBottom) { + scrollableLabel.setText("\u2508"); // dotted line + } else { + scrollableLabel.setText("\u2304"); // down arrow head + } + + radioButtonComposite.layout(true, true); + scrolledComposite.setMinSize(radioButtonComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + + stackLayout.topControl = scrolledComposite; + stackComposite.layout(true, true); + container.layout(true, true); + } + }; + + scrolledComposite.getVerticalBar().addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(final SelectionEvent e) { + showDownArrowWhenScrollable.run(); + } + }); + + startFetchProfilesTask(stackComposite, radioButtonComposite, showDownArrowWhenScrollable); + return container; + } + + private void setupHeaderText(final Composite container) { + titleFont = createFont(14, SWT.BOLD); + + Label headerLabel = new Label(container, SWT.NONE); + headerLabel.setText(HEADER); + headerLabel.setFont(titleFont); + headerLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + descriptionFont = createFont(12, SWT.NORMAL); + + StyledText descriptionText = new StyledText(container, SWT.READ_ONLY | SWT.WRAP); + descriptionText.setText(DESCRIPTION); + descriptionText.setFont(descriptionFont); + descriptionText.setBackground(container.getBackground()); + descriptionText.setEditable(false); + descriptionText.setCaret(null); + GridData textData = new GridData(SWT.FILL, SWT.CENTER, true, false); + descriptionText.setLayoutData(textData); + } + + private Composite setupLoadingComposite(final Composite stackComposite) { + Composite loadingComposite = new Composite(stackComposite, SWT.NONE); + loadingComposite.setLayout(new GridLayout(1, false)); + Label loadingLabel = new Label(loadingComposite, SWT.NONE); + loadingLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + loadingLabel.setText("Loading profiles "); + + loadingLabelFont = createFont(11, SWT.ITALIC); + loadingLabel.setFont(loadingLabelFont); + + Point size = loadingLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT); + + GridData loadingLabelData = new GridData(SWT.CENTER, SWT.CENTER, true, true); + loadingLabelData.verticalIndent = 20; + loadingLabelData.widthHint = size.x; + loadingLabel.setLayoutData(loadingLabelData); + + Display.getDefault().timerExec(250, new Runnable() { + private int dotCount = 0; + + @Override + public void run() { + if (loadingLabel != null && !loadingLabel.isDisposed()) { + dotCount = (dotCount + 1) % 4; + String dots = ".".repeat(dotCount); + loadingLabel.setText("Loading profiles" + dots); + loadingComposite.layout(true); + stackComposite.layout(true, true); + Display.getDefault().timerExec(500, this); + } + } + }); + + loadingLabel.addDisposeListener(e -> { + if (loadingLabel.getFont() != null && !loadingLabel.getFont().isDisposed()) { + loadingLabel.getFont().dispose(); + } + }); + + return loadingComposite; + } + + private void startFetchProfilesTask(final Composite stackComposite, final Composite radioButtonComposite, + final Runnable showDownArrowWhenScrollable) { + Thread updateThread = new Thread() { + @Override + public void run() { + QDeveloperProfileUtil.getInstance().queryForDeveloperProfilesFuture(false) + .thenAccept(profiles -> { + QDeveloperProfile selectedDeveloperProfile = QDeveloperProfileUtil.getInstance().getSelectedProfile(); + + Display.getDefault().asyncExec(() -> { + if (!stackComposite.isDisposed()) { + if (selectedDeveloperProfile != null) { + selectedRadioButton = createRadioButton(radioButtonComposite, selectedDeveloperProfile, + SWT.NONE, true); + } + + for (QDeveloperProfile profile : profiles) { + if (selectedDeveloperProfile == null + || !profile.getArn().equals(selectedDeveloperProfile.getArn())) { + createRadioButton(radioButtonComposite, profile, SWT.NONE, false); + } + } + + showDownArrowWhenScrollable.run(); + } + }); + + }); + } + }; + updateThread.setDaemon(true); + updateThread.start(); + } + @Override + protected void configureShell(final Shell newShell) { + super.configureShell(newShell); + newShell.setText(WINDOW_TITLE); + + newShell.addDisposeListener(e -> { + if (titleFont != null && !titleFont.isDisposed()) { + titleFont.dispose(); + } + + if (descriptionFont != null && !descriptionFont.isDisposed()) { + descriptionFont.dispose(); + } + }); + } + + @Override + protected void okPressed() { + BusyIndicator.showWhile(Display.getDefault(), () -> { + if (selectedRadioButton != null) { + QDeveloperProfileUtil.getInstance() + .setDeveloperProfile((QDeveloperProfile) selectedRadioButton.getData(), true) + .join(); + } + }); + + super.okPressed(); + } + + private RadioButtonWithDescriptor createRadioButton(final Composite parent, + final QDeveloperProfile developerProfile, + final int style, final boolean isSelected) { + RadioButtonWithDescriptor button = new RadioButtonWithDescriptor(parent, developerProfile.getName(), + developerProfile.getRegion(), developerProfile.getAccountId(), style); + button.setData(developerProfile); + button.addSelectionListener(() -> { + if (selectedRadioButton != null && selectedRadioButton != button) { + selectedRadioButton.setSelection(false); + } + selectedRadioButton = button; + }); + + button.setSelection(isSelected); + return button; + } + + @Override + public boolean close() { + if (loadingLabelFont != null && !loadingLabelFont.isDisposed()) { + loadingLabelFont.dispose(); + } + if (scrollableLabelFont != null && !scrollableLabelFont.isDisposed()) { + scrollableLabelFont.dispose(); + } + return super.close(); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/CustomizationDialog.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/CustomizationDialog.java index f2febb250..0a9046810 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/CustomizationDialog.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/CustomizationDialog.java @@ -11,7 +11,6 @@ import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; @@ -30,13 +29,13 @@ import org.eclipse.swt.widgets.Shell; import software.amazon.awssdk.utils.StringUtils; -import software.aws.toolkits.eclipse.amazonq.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.Constants; 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.ToolkitNotification; import software.aws.toolkits.eclipse.amazonq.views.model.Customization; public final class CustomizationDialog extends Dialog { @@ -54,7 +53,7 @@ public enum ResponseSelection { CUSTOMIZATION } - public final class RadioButtonWithDescriptor extends Composite { + private final class RadioButtonWithDescriptor extends Composite { private Button radioButton; private Label textLabel; @@ -62,7 +61,7 @@ public final class RadioButtonWithDescriptor extends Composite { private Font textFont; private Font subtextFont; - public RadioButtonWithDescriptor(final Composite parent, final String text, final String subtext, + private RadioButtonWithDescriptor(final Composite parent, final String text, final String subtext, final int style) { super(parent, SWT.NONE); @@ -115,7 +114,7 @@ public void setSelection(final boolean isSelected) { radioButton.setSelection(isSelected); } - public void addSelectionListener(final Runnable runnable) { + private void addSelectionListener(final Runnable runnable) { radioButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { @@ -172,20 +171,32 @@ private List getCustomizations() { return customizations; } - private static void addFormattedOption(final Combo combo, final String name, final String description) { - String formattedText = name + " (" + description + ")"; + private static void addFormattedOption(final Combo combo, final String name, final String profileName, + final String description) { + String formattedText = name + " (" + profileName + ") - " + description; combo.add(formattedText); } private void updateComboOnUIThread(final List customizations) { combo.removeAll(); - int defaultSelectedDropdownIndex = -1; + int customizationsCount = 0; + int selectedCustomizationIndex = 0; + Customization currentCustomization = Activator.getPluginStore() + .getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class); for (int index = 0; index < customizations.size(); index++) { - addFormattedOption(combo, customizations.get(index).getName(), customizations.get(index).getDescription()); - combo.setData(String.format("%s", index), customizations.get(index)); - defaultSelectedDropdownIndex = index; + if (customizations.get(index).getIsDefault()) { + continue; + } + if (currentCustomization != null + && customizations.get(index).getArn().equals(currentCustomization.getArn())) { + selectedCustomizationIndex = customizationsCount; + } + addFormattedOption(combo, customizations.get(index).getName(), + customizations.get(index).getProfile().getName(), customizations.get(index).getDescription()); + combo.setData(String.format("%s", customizationsCount), customizations.get(index)); + ++customizationsCount; } - combo.select(defaultSelectedDropdownIndex); + combo.select(selectedCustomizationIndex); if (this.responseSelection.equals(ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT) || customizations.isEmpty()) { combo.setEnabled(false); } else { @@ -304,25 +315,26 @@ protected Control createDialogArea(final Composite parent) { protected void okPressed() { if (this.responseSelection.equals(ResponseSelection.AMAZON_Q_FOUNDATION_DEFAULT)) { Activator.getPluginStore().remove(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY); - Display.getCurrent().asyncExec(() -> showNotification(Constants.DEFAULT_Q_FOUNDATION_DISPLAY_NAME)); + Display.getCurrent() + .asyncExec(() -> CustomizationUtil.showNotification(Constants.DEFAULT_Q_FOUNDATION_DISPLAY_NAME)); } else if (Objects.nonNull(this.getSelectedCustomization()) && StringUtils.isNotBlank(this.getSelectedCustomization().getName())) { + try { + QDeveloperProfileUtil.getInstance() + .setDeveloperProfile(this.getSelectedCustomization().getProfile(), false) + .get(); + } catch (InterruptedException | ExecutionException e) { + Activator.getLogger().info("Failed to update profile: " + e); + } Activator.getPluginStore().putObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, this.getSelectedCustomization()); - ThreadingUtils.executeAsyncTask(() -> CustomizationUtil.triggerChangeConfigurationNotification()); - Display.getCurrent().asyncExec(() -> showNotification( + Display.getCurrent().asyncExec(() -> CustomizationUtil.showNotification( String.format("%s customization", this.getSelectedCustomization().getName()))); } + ThreadingUtils.executeAsyncTask(() -> CustomizationUtil.triggerChangeConfigurationNotification()); super.okPressed(); } - private void showNotification(final String customizationName) { - AbstractNotificationPopup notification = new ToolkitNotification(Display.getCurrent(), - Constants.IDE_CUSTOMIZATION_NOTIFICATION_TITLE, - String.format(Constants.IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE, customizationName)); - notification.open(); - } - private Font createFont(final int size, final int style) { FontData[] fontData = getShell().getDisplay().getSystemFont().getFontData(); FontData newFontData = new FontData(fontData[0].getName(), size, // specify exact font size diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/DialogContributionItem.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/DialogContributionItem.java index 1f53a17de..590112749 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/DialogContributionItem.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/DialogContributionItem.java @@ -23,12 +23,6 @@ public DialogContributionItem(final Dialog dialog, final String menuItemName) { this.menuItemName = menuItemName; } - public DialogContributionItem(final Dialog dialog, final String menuItemName, final Image icon) { - this.dialog = dialog; - this.menuItemName = menuItemName; - this.icon = icon; - } - @Override public final void fill(final Menu menu, final int index) { MenuItem menuItem = new MenuItem(menu, SWT.NONE, index); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/FeedbackDialog.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/FeedbackDialog.java index 72b8d1046..8cf277243 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/FeedbackDialog.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/FeedbackDialog.java @@ -50,7 +50,7 @@ import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; -public class FeedbackDialog extends Dialog { +public final class FeedbackDialog extends Dialog { private static final String TITLE = "Share Feedback"; private static final int MAX_CHAR_LIMIT = 2000; @@ -62,11 +62,11 @@ public class FeedbackDialog extends Dialog { private Sentiment selectedSentiment = Sentiment.POSITIVE; private boolean isCommentQuestionGhostLabelVisible = true; - public class CustomRadioButton extends Composite { + private final class CustomRadioButton extends Composite { private final Label iconLabel; private final Button radioButton; - public CustomRadioButton(final Composite parent, final Image image, final int style) { + private CustomRadioButton(final Composite parent, final Image image, final int style) { super(parent, style); Composite contentComposite = new Composite(parent, SWT.NONE); @@ -85,7 +85,7 @@ public CustomRadioButton(final Composite parent, final Image image, final int st radioButton.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); } - public final Button getRadioButton() { + public Button getRadioButton() { return radioButton; } } @@ -112,13 +112,13 @@ private Image loadImage(final String imagePath) { } @Override - protected final void createButtonsForButtonBar(final Composite parent) { + protected void createButtonsForButtonBar(final Composite parent) { createButton(parent, IDialogConstants.OK_ID, "Share", true); createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); } @Override - protected final void okPressed() { + protected void okPressed() { Sentiment selectedSentiment = this.selectedSentiment; String comment = commentBox.getText(); ThreadingUtils.executeAsyncTask(() -> Activator.getTelemetryService().emitFeedback(comment, selectedSentiment)); @@ -127,7 +127,7 @@ protected final void okPressed() { } @Override - protected final void cancelPressed() { + protected void cancelPressed() { UiTelemetryProvider.emitClickEventMetric("feedback_shareFeedbackDialogCancelButton"); super.cancelPressed(); } @@ -142,7 +142,7 @@ private void handleTextModified(final ModifyEvent event) { } @Override - protected final Control createDialogArea(final Composite parent) { + protected Control createDialogArea(final Composite parent) { container = (Composite) super.createDialogArea(parent); GridLayout layout = new GridLayout(1, false); layout.marginLeft = 10; @@ -472,13 +472,13 @@ private void updateCharacterRemainingCount() { } @Override - protected final void configureShell(final Shell newShell) { + protected void configureShell(final Shell newShell) { super.configureShell(newShell); newShell.setText(TITLE); } @Override - protected final Point getInitialSize() { + protected Point getInitialSize() { return PluginUtils.getPlatform().equals(PluginPlatform.WINDOWS) ? new Point(800, 670) : new Point(800, 620); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/LoginViewActionHandler.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/LoginViewActionHandler.java index 87c39d0ac..bc37dc862 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/LoginViewActionHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/LoginViewActionHandler.java @@ -3,13 +3,17 @@ package software.aws.toolkits.eclipse.amazonq.views; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Future; import java.util.stream.Collectors; import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.widgets.Display; import software.amazon.awssdk.regions.servicemetadata.OidcServiceMetadata; import software.amazon.awssdk.utils.StringUtils; +import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginIdcParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; @@ -20,6 +24,7 @@ import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.views.model.Command; import software.aws.toolkits.eclipse.amazonq.views.model.ParsedCommand; +import software.aws.toolkits.eclipse.amazonq.views.model.QDeveloperProfile; public class LoginViewActionHandler implements ViewActionHandler { @@ -51,6 +56,15 @@ public final void handleCommand(final ParsedCommand parsedCommand, final Browser } Activator.getLoginService().login(LoginType.IAM_IDENTITY_CENTER, new LoginParams().setLoginIdcParams(loginIdcParams)).get(); + if (QDeveloperProfileUtil.getInstance().isProfileSelectionRequired()) { + Map profilesData = new HashMap<>(); + profilesData.put("profiles", + QDeveloperProfileUtil.getInstance().getDeveloperProfiles()); + Display.getDefault().asyncExec(() -> { + browser.execute(String.format("ideClient.handleProfiles(%s)", + JSON_HANDLER.serialize(profilesData))); + }); + } } isLoginTaskRunning = false; } catch (Exception e) { @@ -82,13 +96,17 @@ public final void handleCommand(final ParsedCommand parsedCommand, final Browser region: 'us-east-1' }, feature: 'q', - existConnections: [] + existConnections: [], + profiles: [] } """, "START", regions).stripIndent(); browser.execute("changeTheme(" + THEME_DETECTOR.isDarkTheme() + ");"); browser.execute(String.format("ideClient.prepareUi(%s)", js)); browser.execute("ideClient.updateAuthorization('')"); - browser.execute("document.oncontextmenu = e => e.preventDefault();"); + break; + case ON_SELECT_PROFILE: + QDeveloperProfile developerProfile = JSON_HANDLER.convertObject(params, QDeveloperProfile.class); + QDeveloperProfileUtil.getInstance().setDeveloperProfile(developerProfile, true); break; default: Activator.getLogger() 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 01c4906c6..e1f0826b2 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java @@ -3,68 +3,82 @@ package software.aws.toolkits.eclipse.amazonq.views; +import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.ProgressAdapter; import org.eclipse.swt.browser.ProgressEvent; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.providers.assets.ToolkitLoginWebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.providers.assets.WebViewAssetProvider; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQViewCommonActions; +import software.aws.toolkits.eclipse.amazonq.views.model.UpdateRedirectUrlCommand; -public final class ToolkitLoginWebview extends AmazonQView { +public final class ToolkitLoginWebview extends AmazonQView implements EventObserver { public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview"; private AmazonQViewCommonActions amazonQCommonActions; - + private Browser browser; private final WebViewAssetProvider webViewAssetProvider; public ToolkitLoginWebview() { super(); webViewAssetProvider = new ToolkitLoginWebViewAssetProvider(); webViewAssetProvider.initialize(); + Activator.getEventBroker().subscribe(UpdateRedirectUrlCommand.class, this); } @Override public Composite setupView(final Composite parent) { super.setupView(parent); - setupParentBackground(parent); - var result = setupBrowser(parent); - if (!result) { - return parent; - } - var browser = getBrowser(); - - browser.setVisible(false); - browser.addProgressListener(new ProgressAdapter() { - @Override - public void completed(final ProgressEvent event) { - Display.getDefault().asyncExec(() -> { - if (!browser.isDisposed()) { - browser.setVisible(true); - } - }); + browser = getAndAttachBrowser(parent); + + if (browser == null || browser.isDisposed()) { + browser = setupBrowser(parent); + if (browser == null) { + return parent; } - }); - webViewAssetProvider.injectAssets(browser); - addFocusListener(parent, browser); + browser.setVisible(false); + browser.addProgressListener(new ProgressAdapter() { + @Override + public void completed(final ProgressEvent event) { + Display.getDefault().asyncExec(() -> { + if (!browser.isDisposed()) { + browser.setVisible(true); + } + }); + } + }); + + webViewAssetProvider.injectAssets(browser); + } + addFocusListener(parent, browser); amazonQCommonActions = getAmazonQCommonActions(); setupAmazonQCommonActions(); + parent.addDisposeListener(e -> this.preserveBrowser()); + return parent; } @Override public void dispose() { - var browser = getBrowser(); - if (browser != null && !browser.isDisposed()) { - browser.dispose(); - } super.dispose(); } + + @Override + public void onEvent(final UpdateRedirectUrlCommand redirectUrlCommand) { + Display.getDefault().asyncExec(() -> { + var browser = getBrowser(); + String command = "ideClient.updateRedirectUrl('" + redirectUrlCommand.redirectUrl() + "')"; + browser.execute(command); + }); + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ViewConstants.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ViewConstants.java index 6cfe4669b..37a394665 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ViewConstants.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ViewConstants.java @@ -10,4 +10,6 @@ private ViewConstants() { public static final String COMMAND_FUNCTION_NAME = "ideCommand"; public static final String PREFERENCE_STORE_PLUGIN_FIRST_STARTUP_KEY = "qEclipseFirstLoad"; + public static final String Q_DEVELOPER_PROFILE_SELECTION_KEY = "qDeveloperProfileSelection"; + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/AmazonQAbstractCommonActions.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/AmazonQAbstractCommonActions.java index ece0c06d0..ef9cd959f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/AmazonQAbstractCommonActions.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/AmazonQAbstractCommonActions.java @@ -28,7 +28,9 @@ protected static final class Actions { private final OpenUserGuideAction openUserGuideAction; private final ViewSourceAction viewSourceAction; private final ViewLogsAction viewLogsAction; + private final ChangeProfileDialogContributionItem changeProfileDialogContributionItem; private final ReportAnIssueAction reportAnIssueAction; + private final OpenPreferencesAction openPreferencesAction; Actions() { signoutAction = new SignoutAction(); @@ -41,6 +43,8 @@ protected static final class Actions { viewSourceAction = new ViewSourceAction(); viewLogsAction = new ViewLogsAction(); reportAnIssueAction = new ReportAnIssueAction(); + openPreferencesAction = new OpenPreferencesAction(); + changeProfileDialogContributionItem = new ChangeProfileDialogContributionItem(); } public OpenQChatAction getOpenQChatAction() { @@ -57,6 +61,8 @@ public void setVisibility(final AuthState authState) { // using IAM identity center customizationDialogContributionItem.setVisible( authState.isLoggedIn() && authState.loginType().equals(LoginType.IAM_IDENTITY_CENTER)); + changeProfileDialogContributionItem.setVisible( + authState.isLoggedIn() && authState.loginType().equals(LoginType.IAM_IDENTITY_CENTER)); }); } @@ -85,7 +91,6 @@ protected final void addCommonMenuItems(final IMenuManager menuManager, final Ac if (includeToggleAutoTriggerContributionItem) { menuManager.add(action.toggleAutoTriggerContributionItem); } - menuManager.add(new ContributionItem(action.customizationDialogContributionItem.getId()) { @Override public boolean isVisible() { @@ -108,9 +113,31 @@ public void fill(final ToolBar parent, final int index) { } }); menuManager.add(new Separator()); + menuManager.add(action.openPreferencesAction); menuManager.add(feedbackSubMenu); menuManager.add(helpSubMenu); menuManager.add(new Separator()); + menuManager.add(new ContributionItem(action.changeProfileDialogContributionItem.getId()) { + @Override + public boolean isVisible() { + return action.changeProfileDialogContributionItem.isVisible(); + } + + @Override + public void fill(final Menu parent, final int index) { + action.changeProfileDialogContributionItem.fill(parent, index); + } + + @Override + public void fill(final Composite parent) { + action.changeProfileDialogContributionItem.fill(parent); + } + + @Override + public void fill(final ToolBar parent, final int index) { + action.changeProfileDialogContributionItem.fill(parent, index); + } + }); menuManager.add(new ActionContributionItem(action.signoutAction) { @Override public boolean isVisible() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/ChangeProfileDialogContributionItem.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/ChangeProfileDialogContributionItem.java new file mode 100644 index 000000000..e9b154e87 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/ChangeProfileDialogContributionItem.java @@ -0,0 +1,42 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.actions; + +import org.eclipse.jface.action.ContributionItem; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Shell; + +import jakarta.inject.Inject; +import software.aws.toolkits.eclipse.amazonq.views.ChangeProfileDialog; + +public final class ChangeProfileDialogContributionItem extends ContributionItem { + private static final String CHANGE_PROFILE_MENU_TEXT = "Change Profile"; + + @Inject + private Shell shell; + + @Override + public void setVisible(final boolean isVisible) { + super.setVisible(isVisible); + } + + @Override + public void fill(final Menu menu, final int index) { + MenuItem menuItem = new MenuItem(menu, SWT.NONE, index); + menuItem.setText(CHANGE_PROFILE_MENU_TEXT); + menuItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(final SelectionEvent e) { + ChangeProfileDialog dialog = new ChangeProfileDialog(shell); + dialog.open(); + } + }); + } + +} + diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenPreferencesAction.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenPreferencesAction.java new file mode 100644 index 000000000..33269feb0 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenPreferencesAction.java @@ -0,0 +1,21 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.actions; + +import org.eclipse.jface.action.Action; + +import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; + +public class OpenPreferencesAction extends Action { + + public OpenPreferencesAction() { + setText("Preferences"); + } + + @Override + public final void run() { + AmazonQPreferencePage.openPreferencePane(); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenUrlAction.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenUrlAction.java index de9c1f1ae..2694ee631 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenUrlAction.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/OpenUrlAction.java @@ -13,11 +13,6 @@ public class OpenUrlAction extends Action { private String url; private String metadataId; - public OpenUrlAction(final String actionText, final ExternalLink link) { - setText(actionText); - this.url = link.getValue(); - } - public OpenUrlAction(final String actionText, final String metadataId, final ExternalLink link) { setText(actionText); this.url = link.getValue(); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java index c9a8b87f7..48ffe70d0 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java @@ -5,7 +5,7 @@ import org.eclipse.jface.action.Action; -import software.aws.toolkits.eclipse.amazonq.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ChatCodeReference.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ChatCodeReference.java index 59f881359..a1c11386e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ChatCodeReference.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ChatCodeReference.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.views.model; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CodeReferenceLogItem.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CodeReferenceLogItem.java index ff24196af..a42e86913 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CodeReferenceLogItem.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CodeReferenceLogItem.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.views.model; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Command.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Command.java index 22fc085c2..b0b775e85 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Command.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Command.java @@ -13,7 +13,9 @@ public enum Command { CHAT_TAB_ADD("aws/chat/tabAdd"), CHAT_TAB_REMOVE("aws/chat/tabRemove"), CHAT_TAB_CHANGE("aws/chat/tabChange"), + FILE_CLICK("aws/chat/fileClick"), CHAT_SEND_PROMPT("aws/chat/sendChatPrompt"), + CHAT_PROMPT_OPTION_CHANGE("aws/chat/promptInputOptionChange"), CHAT_LINK_CLICK("aws/chat/linkClick"), CHAT_INFO_LINK_CLICK("aws/chat/infoLinkClick"), CHAT_SOURCE_LINK_CLICK("aws/chat/sourceLinkClick"), @@ -26,12 +28,23 @@ public enum Command { CHAT_INSERT_TO_CURSOR_POSITION("insertToCursorPosition"), AUTH_FOLLOW_UP_CLICKED("authFollowUpClicked"), //Auth command handled in QChat webview DISCLAIMER_ACKNOWLEDGED("disclaimerAcknowledged"), + LIST_CONVERSATIONS("aws/chat/listConversations"), + CONVERSATION_CLICK("aws/chat/conversationClick"), + CREATE_PROMPT("aws/chat/createPrompt"), + PROMPT_OPTION_ACKNOWLEDGED("chatPromptOptionAcknowledged"), + TAB_BAR_ACTION("aws/chat/tabBarAction"), + GET_SERIALIZED_CHAT("aws/chat/getSerializedChat"), + STOP_CHAT_RESPONSE("stopChatResponse"), + BUTTON_CLICK("aws/chat/buttonClick"), + CHAT_OPEN_TAB("aws/chat/openTab"), + OPEN_SETTINGS("openSettings"), // Auth LOGIN_BUILDER_ID("loginBuilderId"), LOGIN_IDC("loginIdC"), CANCEL_LOGIN("cancelLogin"), - ON_LOAD("onLoad"); + ON_LOAD("onLoad"), + ON_SELECT_PROFILE("onSelectProfile"); private final String commandString; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CommandRequest.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CommandRequest.java index 0883775a3..9182e392c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CommandRequest.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/CommandRequest.java @@ -7,11 +7,12 @@ public record CommandRequest( @JsonProperty("command") String commandString, - @JsonProperty("params") Object params) { + @JsonProperty("params") Object params, + @JsonProperty("requestId") String requestId) { public ParsedCommand getParsedCommand() { Command command = Command.fromString(commandString).orElse(null); - ParsedCommand parsedCommand = new ParsedCommand(command, params); + ParsedCommand parsedCommand = new ParsedCommand(command, params, requestId); return parsedCommand; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Configuration.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Configuration.java new file mode 100644 index 000000000..2c01e09d4 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Configuration.java @@ -0,0 +1,76 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.model; + +import com.google.gson.annotations.SerializedName; + +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; + +public class Configuration { + @SerializedName("arn") + private String arn; + + @SerializedName("name") + private String name; + + @SerializedName("accountId") + private String accountId; + + public Configuration(final String arn, final String name) { + this.arn = arn; + this.name = name; + this.accountId = extractAccountId(this.arn); + } + + public Configuration() { + this.arn = null; + this.name = null; + this.accountId = null; + } + + public final void setArn(final String arn) { + this.arn = arn; + } + + public final void setName(final String name) { + this.name = name; + } + + public final void setAccountId(final String accountId) { + this.accountId = accountId; + } + + public final String getArn() { + return this.arn; + } + + public final String getName() { + return this.name; + } + + public final String getAccountId() { + if (this.accountId == null) { + this.accountId = extractAccountId(this.arn); + } + return this.accountId; + } + + private String extractAccountId(final String arn) { + try { + if (arn.trim().isEmpty()) { + return ""; + } + + String[] chunks = arn.split(":"); + + // The 5th chunk is the account id + // eg: arn:aws:codewhisperer:us-west-2:012345678901:profile/ABCDEFGHIJKL + return chunks.length < 5 ? "" : chunks[4]; + } catch (Exception e) { + Activator.getLogger().info(e.getMessage()); + return ""; + } + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Customization.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Customization.java index 2609bec80..f9b0fa1f4 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Customization.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Customization.java @@ -3,26 +3,28 @@ package software.aws.toolkits.eclipse.amazonq.views.model; -public class Customization { - private final String arn; - private final String name; +public class Customization extends Configuration { private final String description; + private final Boolean isDefault; + private final QDeveloperProfile profile; - public Customization(final String arn, final String name, final String description) { - this.arn = arn; - this.name = name; + public Customization(final String arn, final String name, final String description, final Boolean isDefault, + final QDeveloperProfile profile) { + super(arn, name); this.description = description; + this.isDefault = isDefault; + this.profile = profile; } - public final String getArn() { - return this.arn; + public final String getDescription() { + return this.description; } - public final String getName() { - return this.name; + public final Boolean getIsDefault() { + return this.isDefault; } - public final String getDescription() { - return this.description; + public final QDeveloperProfile getProfile() { + return this.profile; } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/IdentityDetails.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/IdentityDetails.java new file mode 100644 index 000000000..f89988940 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/IdentityDetails.java @@ -0,0 +1,10 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.model; + +import com.google.gson.annotations.SerializedName; + +public record IdentityDetails(@SerializedName("region") String region) { + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/InlineSuggestionCodeReference.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/InlineSuggestionCodeReference.java index a1c5ab500..9a10d0b9b 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/InlineSuggestionCodeReference.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/InlineSuggestionCodeReference.java @@ -1,5 +1,5 @@ -//Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -//SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 package software.aws.toolkits.eclipse.amazonq.views.model; diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ParsedCommand.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ParsedCommand.java index 6a7d65e5d..c2743d377 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ParsedCommand.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/ParsedCommand.java @@ -7,10 +7,12 @@ public class ParsedCommand { private final Command command; private final Object params; + private final String requestId; - public ParsedCommand(final Command command, final Object params) { + public ParsedCommand(final Command command, final Object params, final String requestId) { this.command = command; this.params = params; + this.requestId = requestId; } public final Command getCommand() { @@ -21,4 +23,8 @@ public final Object getParams() { return this.params; } + public final String getRequestId() { + return this.requestId; + } + } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/QDeveloperProfile.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/QDeveloperProfile.java new file mode 100644 index 000000000..d4d9a6085 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/QDeveloperProfile.java @@ -0,0 +1,37 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.gson.annotations.SerializedName; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class QDeveloperProfile extends Configuration { + + @SerializedName("identityDetails") + private IdentityDetails identityDetails; + + public QDeveloperProfile(final String arn, final String name, final IdentityDetails identityDetails) { + super(arn, name); + this.identityDetails = identityDetails; + } + + public QDeveloperProfile() { + super(); + this.identityDetails = null; + } + + public final void setIdentityDetails(final IdentityDetails identityDetails) { + this.identityDetails = identityDetails; + } + + public final IdentityDetails getIdentityDetails() { + return this.identityDetails; + } + + public final String getRegion() { + return identityDetails.region(); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateConfigurationParams.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateConfigurationParams.java new file mode 100644 index 000000000..38aebbf20 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateConfigurationParams.java @@ -0,0 +1,26 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.model; + +import java.util.Map; + +public final class UpdateConfigurationParams { + + private String section; + private Map settings; + + public UpdateConfigurationParams(final String section, final Map settings) { + this.section = section; + this.settings = settings; + } + + public String getSection() { + return section; + } + + public Map getSettings() { + return settings; + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateRedirectUrlCommand.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateRedirectUrlCommand.java new file mode 100644 index 000000000..bc35b8326 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/UpdateRedirectUrlCommand.java @@ -0,0 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.views.model; + +public record UpdateRedirectUrlCommand(String redirectUrl) { } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java index 4c1829d76..372f6a8c8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouter.java @@ -10,6 +10,7 @@ import software.aws.toolkits.eclipse.amazonq.broker.events.AmazonQViewType; import software.aws.toolkits.eclipse.amazonq.broker.events.BrowserCompatibilityState; import software.aws.toolkits.eclipse.amazonq.broker.events.ChatWebViewAssetState; +import software.aws.toolkits.eclipse.amazonq.broker.events.QDeveloperProfileState; import software.aws.toolkits.eclipse.amazonq.broker.events.ToolkitLoginWebViewAssetState; import software.aws.toolkits.eclipse.amazonq.broker.events.ViewRouterPluginState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; @@ -28,6 +29,7 @@ public final class ViewRouter implements EventObserver { private AmazonQViewType activeView; + private boolean publishUnconditionally = false; /** * Constructs a ViewRouter with the specified builder configuration. Initializes @@ -62,6 +64,11 @@ private ViewRouter(final Builder builder) { builder.toolkitLoginWebViewAssetStateObservable = Activator.getEventBroker() .ofObservable(ToolkitLoginWebViewAssetState.class); } + + if (builder.qDeveloperProfileStateObservable == null) { + builder.qDeveloperProfileStateObservable = Activator.getEventBroker() + .ofObservable(QDeveloperProfileState.class); + } /** * Combines all state observables into a single stream that emits a new PluginState * whenever any individual state changes. The combined stream: @@ -70,8 +77,8 @@ private ViewRouter(final Builder builder) { */ Observable.combineLatest(builder.authStateObservable, builder.lspStateObservable, builder.browserCompatibilityStateObservable, builder.chatWebViewAssetStateObservable, - builder.toolkitLoginWebViewAssetStateObservable, ViewRouterPluginState::new) - .observeOn(Schedulers.computation()).subscribe(this::onEvent); + builder.toolkitLoginWebViewAssetStateObservable, builder.qDeveloperProfileStateObservable, + ViewRouterPluginState::new).observeOn(Schedulers.computation()).subscribe(this::onEvent); } public static Builder builder() { @@ -115,6 +122,9 @@ private void refreshActiveView(final ViewRouterPluginState pluginState) { } else if (pluginState.authState().isExpired()) { newActiveView = AmazonQViewType.RE_AUTHENTICATE_VIEW; } else { + if (pluginState.qDeveloperProfileState() == QDeveloperProfileState.SELECTED) { + publishUnconditionally = true; + } newActiveView = AmazonQViewType.CHAT_VIEW; } @@ -128,10 +138,11 @@ private void refreshActiveView(final ViewRouterPluginState pluginState) { * @param newActiveViewId The new view to be activated */ private void updateActiveView(final AmazonQViewType newActiveViewId) { - if (activeView != newActiveViewId) { + if (activeView != newActiveViewId || publishUnconditionally) { activeView = newActiveViewId; notifyActiveViewChange(); } + publishUnconditionally = false; } /** @@ -148,6 +159,7 @@ public static final class Builder { private Observable browserCompatibilityStateObservable; private Observable chatWebViewAssetStateObservable; private Observable toolkitLoginWebViewAssetStateObservable; + private Observable qDeveloperProfileStateObservable; public Builder withAuthStateObservable(final Observable authStateObservable) { this.authStateObservable = authStateObservable; @@ -177,6 +189,12 @@ public Builder withToolkitLoginWebViewAssetStateObservable( return this; } + public Builder withQDeveloperProfileStateObservable( + final Observable qDeveloperProfileStateObservable) { + this.qDeveloperProfileStateObservable = qDeveloperProfileStateObservable; + return this; + } + public ViewRouter build() { return new ViewRouter(this); } diff --git a/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java b/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java index 4d35eb2c9..818892a1c 100644 --- a/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java @@ -1,8 +1,13 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.workspace; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; @@ -18,24 +23,23 @@ import org.eclipse.lsp4j.RenameFilesParams; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; -import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; public final class WorkspaceChangeListener implements IResourceChangeListener { + private static final AtomicReference INSTANCE = new AtomicReference<>(); - private static volatile WorkspaceChangeListener instance; + private final FileChangeTracker fileChangeTracker; + private static final Set ALLOWED_RESOURCE_TYPES = Set.of( + IResource.FILE, + IResource.FOLDER); - private WorkspaceChangeListener() { } + private WorkspaceChangeListener() { + this.fileChangeTracker = new FileChangeTracker(); + } public static WorkspaceChangeListener getInstance() { - if (instance == null) { - synchronized (WorkspaceChangeListener.class) { - if (instance == null) { - instance = new WorkspaceChangeListener(); - } - } - } - return instance; + INSTANCE.compareAndSet(null, new WorkspaceChangeListener()); + return INSTANCE.get(); } public void start() { @@ -47,73 +51,98 @@ public void start() { @Override public void resourceChanged(final IResourceChangeEvent event) { - ThreadingUtils.executeAsyncTask(() -> { - boolean indexingEnabled = Activator.getDefault().getPreferenceStore().getBoolean(AmazonQPreferencePage.WORKSPACE_INDEX); - if (!indexingEnabled) { - return; + ThreadingUtils.executeAsyncTask(() -> processResourceChange(event)); + } + + private void processResourceChange(final IResourceChangeEvent event) { + try { + FileChanges changes = fileChangeTracker.trackChanges(event.getDelta()); + notifyLspServer(changes); + } catch (Exception e) { + Activator.getLogger().error("Error processing workspace changes", e); + } + } + + public void stop() { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); + } + + private record FileChanges( + List created, + List deleted, + List renamed + ) { + FileChanges() { + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } + } + + private static final class FileChangeTracker { + FileChanges trackChanges(final IResourceDelta delta) throws CoreException { + FileChanges changes = new FileChanges(); + + delta.accept(resourceDelta -> { + if (!ALLOWED_RESOURCE_TYPES.contains(resourceDelta.getResource().getType())) { + return true; + } - IResourceDelta delta = event.getDelta(); + processResourceDelta(resourceDelta, changes); + return true; + }); - List createdFiles = new ArrayList<>(); - List deletedFiles = new ArrayList<>(); - List renamedFiles = new ArrayList<>(); + return changes; + } + private void processResourceDelta(final IResourceDelta delta, final FileChanges changes) { try { - delta.accept(delta1 -> { - if (delta1.getResource().getType() != IResource.FILE) { - return true; - } - - URI uri = delta1.getResource().getLocationURI(); - String uriString = uri.toString(); - - switch (delta1.getKind()) { - case IResourceDelta.ADDED: - createdFiles.add(new FileCreate(uriString)); - break; - case IResourceDelta.REMOVED: - deletedFiles.add(new FileDelete(uriString)); - break; - case IResourceDelta.CHANGED: - if ((delta1.getFlags() & IResourceDelta.MOVED_FROM) != 0) { - URI oldUri = delta1.getMovedFromPath().toFile().toURI(); - renamedFiles.add(new FileRename(oldUri.toString(), uriString)); - } - break; - default: - } - return true; - }); - } catch (CoreException e) { - Activator.getLogger().error("Unable to process file change events", e); + URI uri = delta.getResource().getLocationURI(); + String uriString = uri.toString(); + + switch (delta.getKind()) { + case IResourceDelta.ADDED: + changes.created.add(new FileCreate(uriString)); + break; + case IResourceDelta.REMOVED: + changes.deleted.add(new FileDelete(uriString)); + break; + case IResourceDelta.CHANGED: + processChangedResource(delta, changes, uriString); + break; + default: + throw new IllegalStateException("Unsupported resource delta type: " + delta.getKind()); + } } catch (IllegalArgumentException e) { Activator.getLogger().error("Invalid resource path", e); } + } - try { - if (!createdFiles.isEmpty()) { - CreateFilesParams createParams = new CreateFilesParams(createdFiles); - Activator.getLspProvider().getAmazonQServer().get().getWorkspaceService().didCreateFiles(createParams); - } + private void processChangedResource(final IResourceDelta delta, final FileChanges changes, final String newUriString) { + if ((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) { + URI oldUri = delta.getMovedFromPath().toFile().toURI(); + changes.renamed.add(new FileRename(oldUri.toString(), newUriString)); + } + } + } - if (!deletedFiles.isEmpty()) { - DeleteFilesParams deleteParams = new DeleteFilesParams(deletedFiles); - Activator.getLspProvider().getAmazonQServer().get().getWorkspaceService().didDeleteFiles(deleteParams); - } + private void notifyLspServer(final FileChanges changes) { + try { + var lspServer = Activator.getLspProvider().getAmazonQServer().get().getWorkspaceService(); - if (!renamedFiles.isEmpty()) { - RenameFilesParams renameParams = new RenameFilesParams(renamedFiles); - Activator.getLspProvider().getAmazonQServer().get().getWorkspaceService().didRenameFiles(renameParams); - } - } catch (Exception e) { - Activator.getLogger().error("Unable to update LSP with file change events: " + e.getMessage()); + if (!changes.created.isEmpty()) { + lspServer.didCreateFiles(new CreateFilesParams(changes.created)); } - }); - } - public void stop() { - ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); - } + if (!changes.deleted.isEmpty()) { + lspServer.didDeleteFiles(new DeleteFilesParams(changes.deleted)); + } + if (!changes.renamed.isEmpty()) { + lspServer.didRenameFiles(new RenameFilesParams(changes.renamed)); + } + } catch (Exception e) { + Activator.getLogger().error( + "Unable to update LSP with file change events: " + e.getMessage() + ); + } + } } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManagerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManagerTest.java index 1cbb4da31..9cdd59321 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManagerTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManagerTest.java @@ -4,11 +4,11 @@ package software.aws.toolkits.eclipse.amazonq.chat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; @@ -16,10 +16,14 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import java.util.Arrays; +import java.lang.reflect.Field; import java.util.Collections; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import org.eclipse.lsp4j.Position; @@ -41,516 +45,614 @@ import software.aws.toolkits.eclipse.amazonq.chat.models.ChatItemAction; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatPrompt; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatRequestParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.ChatResult; import software.aws.toolkits.eclipse.amazonq.chat.models.ChatUIInboundCommand; import software.aws.toolkits.eclipse.amazonq.chat.models.CursorState; import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackParams; import software.aws.toolkits.eclipse.amazonq.chat.models.FeedbackPayload; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUp; import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; import software.aws.toolkits.eclipse.amazonq.chat.models.QuickActionParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.RecommendationContentSpan; -import software.aws.toolkits.eclipse.amazonq.chat.models.ReferenceTrackerInformation; -import software.aws.toolkits.eclipse.amazonq.chat.models.RelatedContent; -import software.aws.toolkits.eclipse.amazonq.chat.models.SourceLink; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; +import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; import software.aws.toolkits.eclipse.amazonq.lsp.encryption.LspEncryptionManager; -import software.aws.toolkits.eclipse.amazonq.util.CodeReferenceLoggingService; +import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; import software.aws.toolkits.eclipse.amazonq.util.JsonHandler; -import software.aws.toolkits.eclipse.amazonq.util.LoggingService; +import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory; import software.aws.toolkits.eclipse.amazonq.util.ProgressNotificationUtils; -import software.aws.toolkits.eclipse.amazonq.views.ChatUiRequestListener; -import software.aws.toolkits.eclipse.amazonq.views.model.ChatCodeReference; import software.aws.toolkits.eclipse.amazonq.views.model.Command; public final class ChatCommunicationManagerTest { + @RegisterExtension + private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); + @Mock private JsonHandler jsonHandler; @Mock private LspEncryptionManager lspEncryptionManager; - @Mock - private ChatMessageProvider chatMessageProvider; - - @Mock - private CompletableFuture chatMessageProviderFuture; - - @Mock - private ChatUiRequestListener chatUiRequestListener; - @Mock private ChatPartialResultMap chatPartialResultMap; @Mock private Display display; - private ChatCommunicationManager chatCommunicationManager; + @Mock + private AmazonQLspServer amazonQLspServer; + private ChatCommunicationManager chatCommunicationManager; @BeforeEach void setupBeforeEach() { MockitoAnnotations.openMocks(this); - // Make sure thenAcceptAsync runs on the main thread - doAnswer(invocation -> { - Consumer consumer = invocation.getArgument(0); - consumer.accept(chatMessageProvider); - return CompletableFuture.completedFuture(null); - }).when(chatMessageProviderFuture).thenAcceptAsync(ArgumentMatchers.>any(), any()); chatCommunicationManager = spy(ChatCommunicationManager.builder() .withJsonHandler(jsonHandler) .withLspEncryptionManager(lspEncryptionManager) - .withChatMessageProvider(chatMessageProviderFuture) .withChatPartialResultMap(chatPartialResultMap) .build()); - when(lspEncryptionManager.encrypt(any(String.class))).thenReturn("encrypted-message"); - when(lspEncryptionManager.decrypt(any(String.class))).thenReturn("decrypted response"); - - } - - @Nested - class SendChatPromptTests { - - @RegisterExtension - private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); - - private final CursorState cursorState = new CursorState(new Range(new Position(0, 0), new Position(1, 1))); - - private final ChatRequestParams params = new ChatRequestParams( - "tabId", - new ChatPrompt("prompt", "escaped prompt", "command", Collections.emptyList()), - new TextDocumentIdentifier("textDocument"), - Arrays.asList(cursorState), - Collections.emptyList() - ); - - private final ChatItemAction chatItemAction = new ChatItemAction( - "pillText", "prompt", false, "description", "button" - ); - - private final ReferenceTrackerInformation referenceTracker = new ReferenceTrackerInformation( - "licenseName", - "repository", - "url", - new RecommendationContentSpan(1, 2), - "information" - ); - - private final ChatResult chatResult = new ChatResult( - "body", - "messageId", - true, - new RelatedContent("title", new SourceLink[]{new SourceLink("title", "url", "body")}), - new FollowUp("text", new ChatItemAction[]{chatItemAction}), - new ReferenceTrackerInformation[]{referenceTracker} - ); - - private CodeReferenceLoggingService codeReferenceLoggingService; - - @BeforeEach - void setupBeforeEach() { - chatCommunicationManager.setChatUiRequestListener(chatUiRequestListener); - codeReferenceLoggingService = activatorStaticMockExtension.getMock(CodeReferenceLoggingService.class); - doReturn(Optional.of("fileUri")).when(chatCommunicationManager).getOpenFileUri(); - doReturn(Optional.of(cursorState)).when(chatCommunicationManager).getSelectionRangeCursorState(); - } - - @Test - void testChatSendPrompt() { - when(jsonHandler.convertObject(any(Object.class), eq(ChatRequestParams.class))) - .thenReturn(params); - when(chatMessageProvider.sendChatPrompt(any(String.class), any(EncryptedChatParams.class))) - .thenReturn(CompletableFuture.completedFuture("chat response")); - - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenReturn(chatResult); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); - - try (MockedStatic displayMock = mockStatic(Display.class)) { - displayMock.when(Display::getDefault).thenReturn(display); - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, params); - } - - verify(jsonHandler).convertObject(any(Object.class), eq(ChatRequestParams.class)); - verify(chatPartialResultMap).setEntry(any(String.class), eq("tabId")); - verify(chatPartialResultMap).removeEntry(any(String.class)); - - verify(lspEncryptionManager).encrypt(params); - verify(chatMessageProvider).sendChatPrompt(eq("tabId"), any(EncryptedChatParams.class)); - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - verify(codeReferenceLoggingService).log(any(ChatCodeReference.class)); - } - - @Test - void testChatSendPromptWithErrorCommunicatingWithServer() { - when(jsonHandler.convertObject(any(Object.class), eq(ChatRequestParams.class))) - .thenReturn(params); - when(chatMessageProvider.sendChatPrompt(any(String.class), any(EncryptedChatParams.class))) - .thenReturn(CompletableFuture.failedFuture(new RuntimeException("error message"))); - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenReturn(chatResult); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); - - try (MockedStatic displayMock = mockStatic(Display.class)) { - displayMock.when(Display::getDefault).thenReturn(display); - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, params); - } - - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - verify(activatorStaticMockExtension.getMock(LoggingService.class)).error(argThat(message -> - message.contains("An error occurred while processing chat request"))); - } - - @Test - void testChatSendPromptWithErrorInResponse() { - when(jsonHandler.convertObject(any(Object.class), eq(ChatRequestParams.class))) - .thenReturn(params); - when(chatMessageProvider.sendChatPrompt(any(String.class), any(EncryptedChatParams.class))) - .thenReturn(CompletableFuture.completedFuture("chat response")); - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenThrow(new RuntimeException("Test exception")); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); + when(lspEncryptionManager.encrypt(anyString())).thenReturn("encrypted-message"); + when(lspEncryptionManager.decrypt(anyString())).thenReturn("decrypted response"); - try (MockedStatic displayMock = mockStatic(Display.class)) { - displayMock.when(Display::getDefault).thenReturn(display); - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, params); - } - - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - verify(activatorStaticMockExtension.getMock(LoggingService.class)).error(argThat(message -> - message.contains("An error occurred while processing chat response"))); - } + CompletableFuture serverFuture = mock(CompletableFuture.class); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(amazonQLspServer); + return CompletableFuture.completedFuture(null); + }).when(serverFuture).thenAcceptAsync(ArgumentMatchers.>any(), any()); + doAnswer(invocation -> Optional.of("fileUri")).when(chatCommunicationManager).getOpenFileUri(); + CursorState cursorState = new CursorState(new Range(new Position(0, 0), new Position(1, 1))); + doAnswer(invocation -> Optional.of(cursorState)).when(chatCommunicationManager).getSelectionRangeCursorState(); } @Nested - class SendQuickActionsTests { - - @RegisterExtension - private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); - - private final QuickActionParams quickActionParams = new QuickActionParams("tabId", "quickAction", "prompt"); - - private final ChatItemAction chatItemAction = new ChatItemAction( - "pillText", "prompt", false, "description", "button" - ); - - private final ReferenceTrackerInformation referenceTracker = new ReferenceTrackerInformation( - "licenseName", - "repository", - "url", - new RecommendationContentSpan(1, 2), - "information" - ); - - private final ChatResult chatResult = new ChatResult( - "body", - "messageId", - true, - new RelatedContent("title", new SourceLink[]{new SourceLink("title", "url", "body")}), - new FollowUp("text", new ChatItemAction[]{chatItemAction}), - new ReferenceTrackerInformation[]{referenceTracker} - ); - - @BeforeEach - void setupBeforeEach() { - chatCommunicationManager.setChatUiRequestListener(chatUiRequestListener); - } - + class RequestManagementTests { @Test - void testSendQuickAction() { - when(jsonHandler.convertObject(any(Object.class), eq(QuickActionParams.class))) - .thenReturn(quickActionParams); - when(chatMessageProvider.sendQuickAction(any(String.class), any(EncryptedQuickActionParams.class))) - .thenReturn(CompletableFuture.completedFuture("chat response")); + void testCancelInflightRequests() throws Exception { + CompletableFuture mockFuture = mock(CompletableFuture.class); - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenReturn(chatResult); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); + Map> inflightRequestMap = new ConcurrentHashMap<>(); + inflightRequestMap.put("tabId", mockFuture); - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_QUICK_ACTION, quickActionParams); + Field inflightRequestField = ChatCommunicationManager.class.getDeclaredField("inflightRequestByTabId"); + inflightRequestField.setAccessible(true); + inflightRequestField.set(chatCommunicationManager, inflightRequestMap); - verify(jsonHandler).convertObject(any(Object.class), eq(QuickActionParams.class)); - verify(chatPartialResultMap).setEntry(any(String.class), eq("tabId")); - verify(chatPartialResultMap).removeEntry(any(String.class)); + chatCommunicationManager.cancelInflightRequests("tabId"); - verify(lspEncryptionManager).encrypt(quickActionParams); - verify(chatMessageProvider).sendQuickAction(eq("tabId"), any(EncryptedQuickActionParams.class)); - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); + verify(mockFuture).cancel(true); + assertTrue(inflightRequestMap.isEmpty()); } - - @Test - void testChatSendPromptWithErrorCommunicatingWithServer() { - when(jsonHandler.convertObject(any(Object.class), eq(QuickActionParams.class))) - .thenReturn(quickActionParams); - when(chatMessageProvider.sendQuickAction(any(String.class), any(EncryptedQuickActionParams.class))) - .thenReturn(CompletableFuture.failedFuture(new RuntimeException("error message"))); - - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenReturn(chatResult); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); - - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_QUICK_ACTION, quickActionParams); - - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - verify(activatorStaticMockExtension.getMock(LoggingService.class)).error(argThat(message -> - message.contains("An error occurred while processing chat request"))); - } - - @Test - void testChatSendPromptWithErrorInResponse() { - when(jsonHandler.convertObject(any(Object.class), eq(QuickActionParams.class))) - .thenReturn(quickActionParams); - when(chatMessageProvider.sendQuickAction(any(String.class), any(EncryptedQuickActionParams.class))) - .thenReturn(CompletableFuture.completedFuture("chat response")); - - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))) - .thenThrow(new RuntimeException("Test exception")); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); - - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_QUICK_ACTION, quickActionParams); - - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - verify(activatorStaticMockExtension.getMock(LoggingService.class)).error(argThat(message -> - message.contains("An error occurred while processing chat response"))); - } - } @Nested - class SendChatReadyAndTelemetryEventTests { - - @Test - void testSendQuickAction() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_READY, new Object()); - verify(chatMessageProvider).sendChatReady(); - } + class ChatPromptTests { + private final ChatMessage params = new ChatMessage(new ChatRequestParams("tabId", + new ChatPrompt("prompt", "escaped prompt", "command", Collections.emptyList()), + new TextDocumentIdentifier("textDocument"), Collections.emptyList(), Collections.emptyList())); + + private final String jsonString = "{" + + " \"type\": \"answer\"," + + " \"header\": {" + + " \"type\": \"answer\"," + + " \"body\": \"body\"," + + " \"status\": {" + + " \"status\": \"success\"," + + " \"text\": \"Success\"" + + " }" + + " }," + + " \"buttons\": []," + + " \"body\": \"body\"," + + " \"messageId\": \"messageId\"," + + " \"canBeVoted\": true," + + " \"relatedContent\": {" + + " \"title\": \"title\"," + + " \"content\": [" + + " {" + + " \"title\": \"title\"," + + " \"url\": \"url\"," + + " \"body\": \"body\"" + + " }" + + " ]" + + " }," + + " \"followUp\": {" + + " \"text\": \"text\"," + + " \"options\": [" + + " {" + + " \"pillText\": \"pillText\"," + + " \"prompt\": \"prompt\"," + + " \"isEnabled\": true," + + " \"description\": \"description\"," + + " \"button\": \"button\"" + + " }" + + " ]" + + " }," + + " \"codeReference\": [" + + " {" + + " \"licenseName\": \"licenseName\"," + + " \"repository\": \"repository\"," + + " \"url\": \"url\"," + + " \"contentSpan\": {" + + " \"start\": 1," + + " \"end\": 2" + + " }," + + " \"information\": \"information\"" + + " }" + + " ]" + + "}"; @Test - void testTelemetryEvent() { - chatCommunicationManager.sendMessageToChatServer(Command.TELEMETRY_EVENT, new Object()); - verify(chatMessageProvider).sendTelemetryEvent(any(Object.class)); - } + void testSuccessfulChatPromptSending() throws Exception { + CompletableFuture completedFuture = CompletableFuture.completedFuture("chat response"); - } + when(amazonQLspServer.sendChatPrompt(any(EncryptedChatParams.class))) + .thenReturn(completedFuture); - @Nested - class SendTabUpdateTests { + when(jsonHandler.deserialize(anyString(), eq(Map.class))) + .thenReturn(ObjectMapperFactory.getInstance().readValue(jsonString, Map.class)); - private final GenericTabParams genericTabParams = new GenericTabParams("tabId"); + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); - @BeforeEach - void setupBeforeEach() { - when(jsonHandler.convertObject(any(Object.class), eq(GenericTabParams.class))) - .thenReturn(genericTabParams); - } + try (MockedStatic displayMock = mockStatic(Display.class)) { + displayMock.when(Display::getDefault).thenReturn(display); - @Test - void testChatTabAdd() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_ADD, genericTabParams); - verify(chatMessageProvider).sendTabAdd(genericTabParams); - } + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, params); - @Test - void testChatTabRemove() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_REMOVE, genericTabParams); - verify(chatMessageProvider).sendTabRemove(genericTabParams); - } + Thread.sleep(1000); + } - @Test - void testChatTabChange() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_CHANGE, genericTabParams); - verify(chatMessageProvider).sendTabChange(genericTabParams); + verify(chatPartialResultMap).setEntry(anyString(), eq("tabId")); + verify(chatPartialResultMap).removeEntry(anyString()); + verify(lspEncryptionManager).encrypt(params.getData()); + verify(lspEncryptionManager).decrypt(anyString()); } @Test - void testEndChat() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_END_CHAT, genericTabParams); - verify(chatMessageProvider).endChat(genericTabParams); - } - - } + void testChatPromptWithResponseDeserializationError() throws Exception { + CompletableFuture completedFuture = CompletableFuture.completedFuture("chat response"); - @Nested - class SendFollowUpClickTests { + when(amazonQLspServer.sendChatPrompt(any(EncryptedChatParams.class))).thenReturn(completedFuture); - private final ChatItemAction chatItemAction = new ChatItemAction( - "pillText", "prompt", false, "description", "button" - ); + RuntimeException deserializeException = new RuntimeException("Test exception"); + when(jsonHandler.deserialize(anyString(), eq(Map.class))).thenThrow(deserializeException); - private final FollowUpClickParams followUpClickParams = new FollowUpClickParams("tabId", "messageId", chatItemAction); + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); - @BeforeEach - void setupBeforeEach() { - when(jsonHandler.convertObject(any(Object.class), eq(FollowUpClickParams.class))) - .thenReturn(followUpClickParams); - } + when(lspEncryptionManager.decrypt(anyString())).thenReturn("some-json-data"); - @Test - void testFollowUpClick() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_FOLLOW_UP_CLICK, followUpClickParams); - verify(chatMessageProvider).followUpClick(followUpClickParams); - } - - } - - @Nested - class SendFeedbackTests { + try (MockedStatic displayMock = mockStatic(Display.class)) { + displayMock.when(Display::getDefault).thenReturn(display); - private final FeedbackPayload feedbackPayload = new FeedbackPayload("messageId", "tabId", "selectedOption", "commend"); - private final FeedbackParams feedbackParams = new FeedbackParams("tabId", "eventId", feedbackPayload); + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, params); - @BeforeEach - void setupBeforeEach() { - when(jsonHandler.convertObject(any(Object.class), eq(FeedbackParams.class))) - .thenReturn(feedbackParams); - } + Thread.sleep(1000); + } - @Test - void testFollowUpClick() { - chatCommunicationManager.sendMessageToChatServer(Command.CHAT_FEEDBACK, feedbackParams); - verify(chatMessageProvider).sendFeedback(feedbackParams); + verify(chatPartialResultMap).setEntry(anyString(), eq("tabId")); + verify(chatPartialResultMap).removeEntry(anyString()); + verify(lspEncryptionManager).encrypt(params.getData()); + verify(lspEncryptionManager).decrypt(anyString()); } - } @Nested - class HandlePartialResultProgressNotificationTests { - - @Mock - private ProgressParams progressParams; - - @BeforeEach - void setupBeforeEach() { - MockitoAnnotations.openMocks(this); - chatCommunicationManager.setChatUiRequestListener(chatUiRequestListener); - } - - @Test - void testWithNullTabId() { - try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) - .thenReturn("token"); - when(chatPartialResultMap.getValue(any(String.class))).thenReturn(null); - - chatCommunicationManager.handlePartialResultProgressNotification(progressParams); - - verifyNoInteractions(lspEncryptionManager); - verifyNoInteractions(jsonHandler); - verifyNoInteractions(chatUiRequestListener); - } - } - - @Test - void testWithEmptyTabId() { - try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) - .thenReturn("token"); - when(chatPartialResultMap.getValue(any(String.class))).thenReturn(""); - - chatCommunicationManager.handlePartialResultProgressNotification(progressParams); - - verifyNoInteractions(lspEncryptionManager); - verifyNoInteractions(jsonHandler); - verifyNoInteractions(chatUiRequestListener); - } - } + class SendQuickActionsTests { - @Test - void testIncorrectParamsObject() { - try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) - .thenReturn("token"); - when(chatPartialResultMap.getValue(any(String.class))).thenReturn("tabId"); - - Either either = mock(Either.class); - when(either.isLeft()).thenReturn(true); - when(progressParams.getValue()).thenReturn(either); - - assertThrows(AmazonQPluginException.class, () -> chatCommunicationManager.handlePartialResultProgressNotification(progressParams)); - - verifyNoInteractions(lspEncryptionManager); - verifyNoInteractions(jsonHandler); - verifyNoInteractions(chatUiRequestListener); - } - } + private final ChatMessage quickActionParams = new ChatMessage( + new QuickActionParams("tabId", "quickAction", "prompt")); + + private final String jsonString = "{" + + " \"type\": \"answer\"," + + " \"header\": {" + + " \"type\": \"answer\"," + + " \"body\": \"body\"," + + " \"status\": {" + + " \"status\": \"success\"," + + " \"text\": \"Success\"" + + " }" + + " }," + + " \"buttons\": []," + + " \"body\": \"body\"," + + " \"messageId\": \"messageId\"," + + " \"canBeVoted\": true," + + " \"relatedContent\": {" + + " \"title\": \"title\"," + + " \"content\": [" + + " {" + + " \"title\": \"title\"," + + " \"url\": \"url\"," + + " \"body\": \"body\"" + + " }" + + " ]" + + " }," + + " \"followUp\": {" + + " \"text\": \"text\"," + + " \"options\": [" + + " {" + + " \"pillText\": \"pillText\"," + + " \"prompt\": \"prompt\"," + + " \"isEnabled\": false," + + " \"description\": \"description\"," + + " \"button\": \"button\"" + + " }" + + " ]" + + " }," + + " \"codeReference\": [" + + " {" + + " \"licenseName\": \"licenseName\"," + + " \"repository\": \"repository\"," + + " \"url\": \"url\"," + + " \"contentSpan\": {" + + " \"start\": 1," + + " \"end\": 2" + + " }," + + " \"information\": \"information\"" + + " }" + + " ]" + + "}"; + + @Test + void testSendQuickAction() throws Exception { + CompletableFuture completedFuture = CompletableFuture.completedFuture("chat response"); + + when(amazonQLspServer.sendQuickAction(any(EncryptedQuickActionParams.class))) + .thenReturn(completedFuture); + + when(jsonHandler.deserialize(anyString(), eq(Map.class))) + .thenReturn(ObjectMapperFactory.getInstance().readValue(jsonString, Map.class)); + + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + + when(lspEncryptionManager.decrypt(anyString())).thenReturn("some-json-data"); + + try (MockedStatic displayMock = mockStatic(Display.class)) { + displayMock.when(Display::getDefault).thenReturn(display); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_QUICK_ACTION, quickActionParams); + + Thread.sleep(1000); + } + + verify(chatPartialResultMap).setEntry(anyString(), eq("tabId")); + verify(chatPartialResultMap).removeEntry(anyString()); + verify(lspEncryptionManager).encrypt(eq(quickActionParams.getData())); + } + + @Test + void testChatSendPromptWithErrorInResponse() throws Exception { + CompletableFuture completedFuture = CompletableFuture.completedFuture("chat response"); + + when(amazonQLspServer.sendQuickAction(any(EncryptedQuickActionParams.class))).thenReturn(completedFuture); + + RuntimeException deserializeException = new RuntimeException("Test exception"); + when(jsonHandler.deserialize(anyString(), eq(Map.class))).thenThrow(deserializeException); + + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + + when(lspEncryptionManager.decrypt(anyString())).thenReturn("some-json-data"); + + try (MockedStatic displayMock = mockStatic(Display.class)) { + displayMock.when(Display::getDefault).thenReturn(display); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_QUICK_ACTION, quickActionParams); + + Thread.sleep(1000); + } + + verify(chatPartialResultMap).setEntry(anyString(), eq("tabId")); + verify(chatPartialResultMap).removeEntry(anyString()); + } + } + + @Nested + class SendChatReadyAndTelemetryEventTests { + @Test + void testSendChatReady() throws Exception { + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).chatReady(); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_READY, new ChatMessage(new Object())); + + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + verify(amazonQLspServer).chatReady(); + } + + @Test + void testTelemetryEvent() throws Exception { + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); - @Test - void testIncorrectChatPartialResult() { - try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) - .thenReturn("token"); - when(chatPartialResultMap.getValue(any(String.class))).thenReturn("tabId"); + ChatMessage message = new ChatMessage(new Object()); - Either either = mock(Either.class); - when(either.getRight()).thenReturn(new Object()); - when(progressParams.getValue()).thenReturn(either); + try (MockedStatic displayMock = mockStatic(Display.class)) { + displayMock.when(Display::getDefault).thenReturn(display); - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getObject(any(ProgressParams.class), eq(String.class))) - .thenReturn("chatPartialResult"); + chatCommunicationManager.sendMessageToChatServer(Command.TELEMETRY_EVENT, message); - ChatResult chatResult = mock(ChatResult.class); + Thread.sleep(1000); + } - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))).thenReturn(chatResult); - when(chatResult.body()).thenReturn(null); + verify(amazonQLspServer).sendTelemetryEvent(message.getData()); + } + } - chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + @Nested + class SendTabUpdateTests { + private final ChatMessage genericTabParams = new ChatMessage(new GenericTabParams("tabId")); - verifyNoInteractions(chatUiRequestListener); - } - } + @BeforeEach + void setUp() { + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + } - @Test - void testChatPartialResult() { - try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) - .thenReturn("token"); - when(chatPartialResultMap.getValue(any(String.class))).thenReturn("tabId"); + @Test + void testChatTabAdd() throws Exception { + CountDownLatch latch = new CountDownLatch(1); - Either either = mock(Either.class); - when(either.getRight()).thenReturn(new Object()); - when(progressParams.getValue()).thenReturn(either); + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).tabAdd(genericTabParams.getData()); - progressNotificationUtilsMock - .when(() -> ProgressNotificationUtils.getObject(any(ProgressParams.class), eq(String.class))) - .thenReturn("chatPartialResult"); + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_ADD, genericTabParams); - ChatResult chatResult = mock(ChatResult.class); + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + verify(amazonQLspServer).tabAdd(genericTabParams.getData()); + } - when(jsonHandler.deserialize(any(String.class), eq(ChatResult.class))).thenReturn(chatResult); - when(chatResult.body()).thenReturn("body"); - when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); + @Test + void testChatTabRemove() throws Exception { + CountDownLatch latch = new CountDownLatch(1); - chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).tabRemove(genericTabParams.getData()); - verify(chatUiRequestListener).onSendToChatUi("serializedObject"); - } - } + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_REMOVE, genericTabParams); + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); - } + verify(amazonQLspServer).tabRemove(genericTabParams.getData()); + } - @Test - void sendMessageToChatServerFails() { - when(jsonHandler.convertObject(any(Object.class), eq(ChatRequestParams.class))) - .thenThrow(new RuntimeException("Test exception")); - - try (MockedStatic displayMock = mockStatic(Display.class)) { - displayMock.when(Display::getDefault).thenReturn(display); - assertThrows(AmazonQPluginException.class, () -> chatCommunicationManager.sendMessageToChatServer(Command.CHAT_SEND_PROMPT, new Object())); - } - } + @Test + void testChatTabChange() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).tabChange(genericTabParams.getData()); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_TAB_CHANGE, genericTabParams); + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + verify(amazonQLspServer).tabChange(genericTabParams.getData()); + } + + @Test + void testEndChat() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).endChat(genericTabParams.getData()); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_END_CHAT, genericTabParams); + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + + verify(amazonQLspServer).endChat(genericTabParams.getData()); + } + } + + @Nested + class SendFollowUpClickTests { + private final ChatItemAction chatItemAction = new ChatItemAction("pillText", "prompt", false, "description", + "button"); + + private final ChatMessage followUpClickParams = new ChatMessage( + new FollowUpClickParams("tabId", "messageId", chatItemAction)); + + @BeforeEach + void setupBeforeEach() { + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + } + + @Test + void testFollowUpClick() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).followUpClick(followUpClickParams.getData()); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_FOLLOW_UP_CLICK, followUpClickParams); + + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + verify(amazonQLspServer).followUpClick(followUpClickParams.getData()); + } + } + + @Nested + class SendFeedbackTests { + private final FeedbackPayload feedbackPayload = new FeedbackPayload("messageId", "tabId", "selectedOption", + "commend"); + private final ChatMessage feedbackParams = new ChatMessage( + new FeedbackParams("tabId", "eventId", feedbackPayload)); + + @BeforeEach + void setupBeforeEach() { + CompletableFuture serverFuture = CompletableFuture.completedFuture(amazonQLspServer); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()).thenReturn(serverFuture); + } + + @Test + void testSendFeedback() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(invocation -> { + latch.countDown(); + return null; + }).when(amazonQLspServer).sendFeedback(feedbackParams.getData()); + + chatCommunicationManager.sendMessageToChatServer(Command.CHAT_FEEDBACK, feedbackParams); + + assertTrue(latch.await(2, TimeUnit.SECONDS), "Async operation did not complete in time"); + verify(amazonQLspServer).sendFeedback(feedbackParams.getData()); + } + } + + @Nested + class HandlePartialResultProgressNotificationTests { + @Mock + private ProgressParams progressParams; + + @BeforeEach + void setupBeforeEach() { + MockitoAnnotations.openMocks(this); + CompletableFuture serverFuture = mock(CompletableFuture.class); + when(activatorStaticMockExtension.getMock(LspProvider.class).getAmazonQServer()) + .thenReturn(serverFuture); + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(amazonQLspServer); + return CompletableFuture.completedFuture(null); + }).when(serverFuture).thenAcceptAsync(ArgumentMatchers.>any(), any()); + } + + @Test + void testWithNullTabId() { + try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) + .thenReturn("token"); + when(chatPartialResultMap.getValue(anyString())).thenReturn(null); + + chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + + verifyNoInteractions(lspEncryptionManager); + verifyNoInteractions(jsonHandler); + } + } + + @Test + void testWithEmptyTabId() { + try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) + .thenReturn("token"); + when(chatPartialResultMap.getValue(anyString())).thenReturn(""); + + chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + + verifyNoInteractions(lspEncryptionManager); + verifyNoInteractions(jsonHandler); + } + } + + @Test + void testIncorrectParamsObject() { + ConcurrentHashMap partialResultLocks = new ConcurrentHashMap<>(); + partialResultLocks.put("token", new Object()); + + try { + Field partialResultLocksField = ChatCommunicationManager.class.getDeclaredField("partialResultLocks"); + partialResultLocksField.setAccessible(true); + partialResultLocksField.set(chatCommunicationManager, partialResultLocks); + } catch (Exception e) { + throw new RuntimeException(e); + } + + try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) + .thenReturn("token"); + when(chatPartialResultMap.getValue(anyString())).thenReturn("tabId"); + + Either either = mock(Either.class); + when(either.isLeft()).thenReturn(true); + when(progressParams.getValue()).thenReturn(either); + + assertThrows(AmazonQPluginException.class, + () -> chatCommunicationManager.handlePartialResultProgressNotification(progressParams)); + + verifyNoInteractions(lspEncryptionManager); + verifyNoInteractions(jsonHandler); + } + } + + + @Test + void testIncorrectChatPartialResult() { + try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) + .thenReturn("token"); + when(chatPartialResultMap.getValue(anyString())).thenReturn("tabId"); + + Either either = mock(Either.class); + when(either.getRight()).thenReturn(new Object()); + when(progressParams.getValue()).thenReturn(either); + + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getObject(any(ProgressParams.class), eq(String.class))) + .thenReturn("chatPartialResult"); + + Map chatResult = mock(Map.class); + + when(jsonHandler.deserialize(anyString(), eq(Map.class))).thenReturn(chatResult); + when(chatResult.get(any())).thenReturn(null); + + chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + } + } + + @Test + void testChatPartialResult() { + try (MockedStatic progressNotificationUtilsMock = mockStatic(ProgressNotificationUtils.class)) { + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getToken(any(ProgressParams.class))) + .thenReturn("token"); + when(chatPartialResultMap.getValue(anyString())).thenReturn("tabId"); + + Either either = mock(Either.class); + when(either.getRight()).thenReturn(new Object()); + when(progressParams.getValue()).thenReturn(either); + + progressNotificationUtilsMock + .when(() -> ProgressNotificationUtils.getObject(any(ProgressParams.class), eq(String.class))) + .thenReturn("chatPartialResult"); + + Map chatResult = mock(Map.class); + + when(jsonHandler.deserialize(anyString(), eq(Map.class))).thenReturn(chatResult); + when(chatResult.get("body")).thenReturn("body"); + when(jsonHandler.serialize(any(ChatUIInboundCommand.class))).thenReturn("serializedObject"); + + chatCommunicationManager.handlePartialResultProgressNotification(progressParams); + } + } + } } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageTest.java deleted file mode 100644 index ed691bd3c..000000000 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/ChatMessageTest.java +++ /dev/null @@ -1,108 +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.chat; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.concurrent.CompletableFuture; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedChatParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.EncryptedQuickActionParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.FollowUpClickParams; -import software.aws.toolkits.eclipse.amazonq.chat.models.GenericTabParams; -import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; - -public final class ChatMessageTest { - - @Mock - private AmazonQLspServer amazonQLspServerMock; - - private final CompletableFuture mockResponse = CompletableFuture.completedFuture("testResponse"); - - private ChatMessage chatMessage; - - @BeforeEach - void setupBeforeEach() { - MockitoAnnotations.openMocks(this); - when(amazonQLspServerMock.sendChatPrompt(any(EncryptedChatParams.class))) - .thenReturn(mockResponse); - when(amazonQLspServerMock.sendQuickAction(any(EncryptedQuickActionParams.class))) - .thenReturn(mockResponse); - when(amazonQLspServerMock.endChat(any(GenericTabParams.class))) - .thenReturn(CompletableFuture.completedFuture(true)); - - chatMessage = new ChatMessage(amazonQLspServerMock); - } - - @Test - void testSendChatPrompt() { - EncryptedChatParams params = mock(EncryptedChatParams.class); - CompletableFuture response = chatMessage.sendChatPrompt(params); - - verify(amazonQLspServerMock).sendChatPrompt(params); - assertTrue(response.join().equals("testResponse")); - } - - @Test - void testSendQuickAction() { - EncryptedQuickActionParams params = mock(EncryptedQuickActionParams.class); - CompletableFuture response = chatMessage.sendQuickAction(params); - - verify(amazonQLspServerMock).sendQuickAction(params); - assertTrue(response.join().equals("testResponse")); - } - - @Test - void testEndChat() { - GenericTabParams params = mock(GenericTabParams.class); - CompletableFuture response = chatMessage.endChat(params); - - verify(amazonQLspServerMock).endChat(params); - assertTrue(response.join()); - } - - @Test - void testSendChatReady() { - chatMessage.sendChatReady(); - verify(amazonQLspServerMock).chatReady(); - } - - @Test - void testSendTabAdd() { - GenericTabParams params = mock(GenericTabParams.class); - chatMessage.sendTabAdd(params); - verify(amazonQLspServerMock).tabAdd(params); - } - - @Test - void sendTabRemove() { - GenericTabParams params = mock(GenericTabParams.class); - chatMessage.sendTabRemove(params); - verify(amazonQLspServerMock).tabRemove(params); - } - - @Test - void sendTabChange() { - GenericTabParams params = mock(GenericTabParams.class); - chatMessage.sendTabChange(params); - verify(amazonQLspServerMock).tabChange(params); - } - - @Test - void sendFollowUpClick() { - FollowUpClickParams params = mock(FollowUpClickParams.class); - chatMessage.followUpClick(params); - verify(amazonQLspServerMock).followUpClick(params); - } - -} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPromptTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPromptTest.java index a28613e06..d60bb3cda 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPromptTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatPromptTest.java @@ -52,7 +52,7 @@ void testJsonDeserialization() throws Exception { assertEquals("Test prompt", deserializedPrompt.prompt()); assertEquals("Test escaped prompt", deserializedPrompt.escapedPrompt()); assertEquals("Test command", deserializedPrompt.command()); - assertEquals(Collections.singletonList(new Command("foo", "bar")), deserializedPrompt.context()); + assertEquals("[{\"command\":\"foo\",\"description\":\"bar\"}]", objectMapper.writeValueAsString(deserializedPrompt.context())); } @Test diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResultTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResultTest.java deleted file mode 100644 index 3165d97c1..000000000 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatResultTest.java +++ /dev/null @@ -1,112 +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.chat.models; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class ChatResultTest { - private final ObjectMapper objectMapper = new ObjectMapper(); - - private final String body = "body"; - private final String messageId = "messageId"; - private final boolean canBeVoted = true; - - private final SourceLink sourceLink = new SourceLink("title", "url", "body"); - private final SourceLink[] sourceLinkArray = new SourceLink[] {sourceLink}; - private final RelatedContent relatedContent = new RelatedContent("title", sourceLinkArray); - - private final String pillText = "Click me"; - private final String prompt = "Test prompt"; - private final Boolean disabled = false; - private final String description = "Test description"; - private final String type = "button"; - private final ChatItemAction chatItemAction = new ChatItemAction(pillText, prompt, disabled, description, type); - - private final String text = "text"; - private final ChatItemAction[] options = new ChatItemAction[] {chatItemAction}; - - private final FollowUp followUp = new FollowUp(text, options); - - private final String licenseName = "licenseName"; - private final String repository = "repository"; - private final String url = "url"; - - private final Integer startLine = 1; - private final Integer endLine = 2; - private final RecommendationContentSpan recommendationSpan = new RecommendationContentSpan(startLine, endLine); - - private final String information = "information"; - private final ReferenceTrackerInformation referenceTrackerInformation = new ReferenceTrackerInformation(licenseName, - repository, url, recommendationSpan, information); - - private final ReferenceTrackerInformation[] referenceTrackerInformationList = new ReferenceTrackerInformation[] { - referenceTrackerInformation}; - - @Test - void testRecordConstructionAndGetters() { - ChatResult chatResult = new ChatResult(body, messageId, canBeVoted, relatedContent, followUp, - referenceTrackerInformationList); - - assertEquals(body, chatResult.body()); - assertEquals(messageId, chatResult.messageId()); - assertEquals(canBeVoted, chatResult.canBeVoted()); - assertEquals(relatedContent, chatResult.relatedContent()); - assertEquals(followUp, chatResult.followUp()); - assertEquals(referenceTrackerInformationList, chatResult.codeReference()); - } - - @Test - void testJsonSerialization() throws Exception { - ChatResult chatResult = new ChatResult(body, messageId, canBeVoted, relatedContent, followUp, - referenceTrackerInformationList); - - String serializedObject = objectMapper.writeValueAsString(chatResult); - - assertEquals(serializedObject, "{\"body\":\"body\",\"messageId\":\"messageId\",\"canBeVoted\":true," - + "\"relatedContent\":{\"title\":\"title\",\"content\":[{\"title\":\"title\",\"url\":\"url\",\"body\":\"body\"}]}" - + ",\"followUp\":{\"text\":\"text\",\"options\":[{\"pillText\":\"Click me\",\"prompt\":\"Test prompt\",\"disabled\":false," - + "\"description\":\"Test description\",\"type\":\"button\"}]},\"codeReference\":[{\"licenseName\":\"licenseName\",\"repository\"" - + ":\"repository\",\"url\":\"url\",\"recommendationContentSpan\":{\"start\":1,\"end\":2},\"information\":\"information\"}]}"); - } - - @Test - void testJsonDeserialization() throws Exception { - String json = "{\"body\":\"body\",\"messageId\":\"messageId\",\"canBeVoted\":true," - + "\"relatedContent\":{\"title\":\"title\",\"content\":[{\"title\":\"title\",\"url\":\"url\",\"body\":\"body\"}]}" - + ",\"followUp\":{\"text\":\"text\",\"options\":[{\"pillText\":\"Click me\",\"prompt\":\"Test prompt\",\"disabled\":false," - + "\"description\":\"Test description\",\"type\":\"button\"}]},\"codeReference\":[{\"licenseName\":\"licenseName\",\"repository\"" - + ":\"repository\",\"url\":\"url\",\"recommendationContentSpan\":{\"start\":1,\"end\":2},\"information\":\"information\"}]}"; - - ChatResult deserializedResult = objectMapper.readValue(json, ChatResult.class); - - assertEquals(body, deserializedResult.body()); - assertEquals(messageId, deserializedResult.messageId()); - assertEquals(canBeVoted, deserializedResult.canBeVoted()); - assertEquals(relatedContent.title(), deserializedResult.relatedContent().title()); - - assertEquals(1, deserializedResult.relatedContent().content().length); - assertNotNull(deserializedResult.relatedContent().content()[0]); - - assertEquals(1, deserializedResult.followUp().options().length); - assertNotNull(deserializedResult.followUp().options()[0]); - - assertEquals(1, deserializedResult.codeReference().length); - assertNotNull(deserializedResult.codeReference()[0]); - } - - @Test - void testDeserializationException() throws Exception { - String json = "incorrectly formatted json"; - - assertThrows(JsonParseException.class, () -> objectMapper.readValue(json, ChatResult.class)); - } - -} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandTest.java index dddbacc20..7fceca590 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/ChatUIInboundCommandTest.java @@ -22,71 +22,19 @@ public class ChatUIInboundCommandTest { private final Object params = new Object(); private final Boolean isPartialResult = true; - private final String body = "body"; - private final String messageId = "messageId"; - private final boolean canBeVoted = true; - - private final SourceLink sourceLink = new SourceLink("title", "url", "body"); - private final SourceLink[] sourceLinkArray = new SourceLink[] {sourceLink}; - private final RelatedContent relatedContent = new RelatedContent("title", sourceLinkArray); - - private final String pillText = "Click me"; - private final String prompt = "Test prompt"; - private final Boolean disabled = false; - private final String description = "Test description"; - private final String type = "button"; - private final ChatItemAction chatItemAction = new ChatItemAction(pillText, prompt, disabled, description, type); - - private final String text = "text"; - private final ChatItemAction[] options = new ChatItemAction[] {chatItemAction}; - - private final FollowUp followUp = new FollowUp(text, options); - - private final String licenseName = "licenseName"; - private final String repository = "repository"; - private final String url = "url"; - - private final Integer startLine = 1; - private final Integer endLine = 2; - private final RecommendationContentSpan recommendationSpan = new RecommendationContentSpan(startLine, endLine); - - private final String information = "information"; - private final ReferenceTrackerInformation referenceTrackerInformation = new ReferenceTrackerInformation(licenseName, - repository, url, recommendationSpan, information); - - private final ReferenceTrackerInformation[] referenceTrackerInformationList = new ReferenceTrackerInformation[] { - referenceTrackerInformation}; - private final String selection = "selection"; private final String triggerType = "triggerType"; + private final String requestId = "requestId"; @Test void testRecordConstructionAndGetters() { - ChatUIInboundCommand chatUiInboundCommand = new ChatUIInboundCommand(command, tabId, params, isPartialResult); + ChatUIInboundCommand chatUiInboundCommand = new ChatUIInboundCommand(command, tabId, params, isPartialResult, requestId); assertEquals(command, chatUiInboundCommand.command()); assertEquals(tabId, chatUiInboundCommand.tabId()); assertEquals(params, chatUiInboundCommand.params()); assertEquals(isPartialResult, chatUiInboundCommand.isPartialResult()); - } - - @Test - void testJsonSerialization() throws Exception { - ChatResult chatResult = new ChatResult(body, messageId, canBeVoted, relatedContent, followUp, - referenceTrackerInformationList); - ChatUIInboundCommand chatUiInboundCommand = new ChatUIInboundCommand(command, tabId, chatResult, - isPartialResult); - - String serializedObject = objectMapper.writeValueAsString(chatUiInboundCommand); - - assertEquals( - "{\"command\":\"command\",\"tabId\":\"tabId\",\"params\":{\"body\":\"body\",\"messageId\":\"messageId\"," - + "\"canBeVoted\":true,\"relatedContent\":{\"title\":\"title\",\"content\":[{\"title\":\"title\",\"url\":" - + "\"url\",\"body\":\"body\"}]},\"followUp\":{\"text\":\"text\",\"options\":[{\"pillText\":\"Click me\",\"" - + "prompt\":\"Test prompt\",\"disabled\":false,\"description\":\"Test description\",\"type\":\"button\"}]}," - + "\"codeReference\":[{\"licenseName\":\"licenseName\",\"repository\":\"repository\",\"url\":\"url\"," - + "\"recommendationContentSpan\":{\"start\":1,\"end\":2},\"information\":\"information\"}]},\"isPartialResult\":true}", - serializedObject); + assertEquals(requestId, chatUiInboundCommand.requestId()); } @Test diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParamsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParamsTest.java new file mode 100644 index 000000000..3a06b365a --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/GenericLinkClickParamsTest.java @@ -0,0 +1,83 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.chat.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class GenericLinkClickParamsTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final String tabId = "tabId"; + private final String link = "link"; + private final String eventId = "eventId"; + private final String messageId = "messageId"; + + @Test + void testRecordSettersAndGetters() { + GenericLinkClickParams genericLinkClickParams = new GenericLinkClickParams(); + genericLinkClickParams.setTabId(tabId); + genericLinkClickParams.setLink(link); + genericLinkClickParams.setEventId(eventId); + genericLinkClickParams.setMessageId(messageId); + + assertEquals(tabId, genericLinkClickParams.getTabId()); + assertEquals(link, genericLinkClickParams.getLink()); + assertEquals(eventId, genericLinkClickParams.getEventId()); + assertEquals(messageId, genericLinkClickParams.getMessageId()); +} + + @Test + void testJsonSerialization() throws Exception { + GenericLinkClickParams genericLinkClickParams = new GenericLinkClickParams(); + genericLinkClickParams.setTabId(tabId); + genericLinkClickParams.setLink(link); + genericLinkClickParams.setEventId(eventId); + genericLinkClickParams.setMessageId(messageId); + + String serializedObject = objectMapper.writeValueAsString(genericLinkClickParams); + + assertEquals("{\"tabId\":\"tabId\",\"link\":\"link\",\"eventId\":\"eventId\",\"messageId\":\"messageId\"}", + serializedObject); + } + + @Test + void testJsonDeserialization() throws Exception { + String json = "{\"tabId\":\"tabId\",\"link\":\"link\",\"eventId\":\"eventId\",\"messageId\":\"messageId\"}"; + + GenericLinkClickParams deserializedResult = objectMapper.readValue(json, GenericLinkClickParams.class); + + assertEquals(tabId, deserializedResult.getTabId()); + assertEquals(link, deserializedResult.getLink()); + assertEquals(eventId, deserializedResult.getEventId()); + assertEquals(messageId, deserializedResult.getMessageId()); + } + + @Test + void testJsonDeserializationWithoutMessageId() throws Exception { + String json = "{\"tabId\":\"tabId\",\"link\":\"link\",\"eventId\":\"eventId\"}"; + + GenericLinkClickParams deserializedResult = objectMapper.readValue(json, GenericLinkClickParams.class); + + assertEquals(tabId, deserializedResult.getTabId()); + assertEquals(link, deserializedResult.getLink()); + assertEquals(eventId, deserializedResult.getEventId()); + assertNull(deserializedResult.getMessageId()); + } + + @Test + void testDeserializationException() throws Exception { + String json = "incorrectly formatted json"; + + assertThrows(JsonParseException.class, () -> objectMapper.readValue(json, GenericLinkClickParams.class)); + } + +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParamsTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParamsTest.java deleted file mode 100644 index 30a04fb7a..000000000 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/InfoLinkClickParamsTest.java +++ /dev/null @@ -1,64 +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.chat.models; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class InfoLinkClickParamsTest { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private final String tabId = "tabId"; - private final String link = "link"; - private final String eventId = "eventId"; - - @Test - void testRecordSettersAndGetters() { - InfoLinkClickParams infoLinkClickParams = new InfoLinkClickParams(); - infoLinkClickParams.setTabId(tabId); - infoLinkClickParams.setLink(link); - infoLinkClickParams.setEventId(eventId); - - assertEquals(tabId, infoLinkClickParams.getTabId()); - assertEquals(link, infoLinkClickParams.getLink()); - assertEquals(eventId, infoLinkClickParams.getEventId()); - } - - @Test - void testJsonSerialization() throws Exception { - InfoLinkClickParams infoLinkClickParams = new InfoLinkClickParams(); - infoLinkClickParams.setTabId(tabId); - infoLinkClickParams.setLink(link); - infoLinkClickParams.setEventId(eventId); - - String serializedObject = objectMapper.writeValueAsString(infoLinkClickParams); - - assertEquals("{\"tabId\":\"tabId\",\"link\":\"link\",\"eventId\":\"eventId\"}", serializedObject); - } - - @Test - void testJsonDeserialization() throws Exception { - String json = "{\"tabId\":\"tabId\",\"link\":\"link\",\"eventId\":\"eventId\"}"; - - InfoLinkClickParams deserializedResult = objectMapper.readValue(json, InfoLinkClickParams.class); - - assertEquals(tabId, deserializedResult.getTabId()); - assertEquals(link, deserializedResult.getLink()); - assertEquals(eventId, deserializedResult.getEventId()); - } - - @Test - void testDeserializationException() throws Exception { - String json = "incorrectly formatted json"; - - assertThrows(JsonParseException.class, () -> objectMapper.readValue(json, InfoLinkClickParams.class)); - } - -} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java index 45ae883ef..2fce6d257 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/chat/models/QChatCssVariableTest.java @@ -12,11 +12,6 @@ public class QChatCssVariableTest { - @Test - void testEnumValues() { - assertEquals(28, QChatCssVariable.values().length); - } - @Test void testTextColorValues() { assertEquals("--mynah-color-text-default", QChatCssVariable.TextColorDefault.getValue()); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtilTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtilTest.java index 4e4742349..6a7f10c31 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtilTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/customization/CustomizationUtilTest.java @@ -3,24 +3,6 @@ package software.aws.toolkits.eclipse.amazonq.customization; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.Mockito; -import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; -import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; -import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; -import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; -import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; -import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; -import software.aws.toolkits.eclipse.amazonq.util.LoggingService; -import org.eclipse.lsp4j.DidChangeConfigurationParams; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -31,7 +13,27 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.services.WorkspaceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; +import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; +import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; +import software.aws.toolkits.eclipse.amazonq.lsp.model.GetConfigurationFromServerParams; +import software.aws.toolkits.eclipse.amazonq.lsp.model.LspServerConfigurations; +import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; +import software.aws.toolkits.eclipse.amazonq.util.LoggingService; import software.aws.toolkits.eclipse.amazonq.views.model.Customization; public final class CustomizationUtilTest { @@ -46,7 +48,7 @@ public final class CustomizationUtilTest { private final class ConfigurationResponse { private List customizations = Arrays.asList( - new Customization("arn", "name", "description") + new Customization("arn", "name", "description", true, null) ); public List getCustomizations() { @@ -94,9 +96,9 @@ void testTriggerChangeConfigurationNotificationWithException() { @Test void testListCustomizations() { - Customization validCustomization = new Customization("arn", "name", "description"); - Customization invalidCustomization = new Customization("", "", ""); - Customization otherValidCustomization = new Customization("arn2", "name2", "description2"); + Customization validCustomization = new Customization("arn", "name", "description", true, null); + Customization invalidCustomization = new Customization("", "", "", true, null); + Customization otherValidCustomization = new Customization("arn2", "name2", "description2", true, null); LspServerConfigurations testConfigurationResponse = new LspServerConfigurations(List.of(validCustomization, invalidCustomization, otherValidCustomization)); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java index 27344c944..72cf86ccb 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java @@ -74,6 +74,9 @@ public void beforeEach(final ExtensionContext context) { @Override public void beforeAll(final ExtensionContext context) { + if (activatorStaticMock != null) { + activatorStaticMock.close(); + } activatorStaticMock = mockStatic(Activator.class); } @@ -81,6 +84,7 @@ public void beforeAll(final ExtensionContext context) { public void afterAll(final ExtensionContext context) throws Exception { if (activatorStaticMock != null) { activatorStaticMock.close(); + activatorStaticMock = null; } } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.java index 9866b2083..1aa78412b 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.java @@ -12,11 +12,11 @@ import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; +import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata; import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; @@ -39,12 +39,14 @@ public final void setUp() { @Test void updateTokenCredentialsUnencryptedSuccess() { String accessToken = "accessToken"; + ConnectionMetadata connectionMetadata = mock(ConnectionMetadata.class); boolean isEncrypted = false; when(mockedAmazonQServer.updateTokenCredentials(any())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); - authCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(accessToken, isEncrypted)); + authCredentialsService + .updateTokenCredentials(new UpdateCredentialsPayload(accessToken, connectionMetadata, isEncrypted)); verify(mockedAmazonQServer).updateTokenCredentials(any()); verifyNoMoreInteractions(mockedAmazonQServer); @@ -53,11 +55,13 @@ void updateTokenCredentialsUnencryptedSuccess() { @Test void updateTokenCredentialsEncryptedSuccess() { boolean isEncrypted = true; + ConnectionMetadata connectionMetadata = mock(ConnectionMetadata.class); when(mockedAmazonQServer.updateTokenCredentials(any())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); - authCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload("encryptedToken", isEncrypted)); + authCredentialsService.updateTokenCredentials( + new UpdateCredentialsPayload("encryptedToken", connectionMetadata, isEncrypted)); verify(mockedAmazonQServer).updateTokenCredentials(any()); verifyNoMoreInteractions(mockedAmazonQServer); diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginServiceTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginServiceTest.java index 02bfe6f4b..b2a95ddd1 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginServiceTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultLoginServiceTest.java @@ -17,7 +17,6 @@ import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,6 +24,7 @@ import software.aws.toolkits.eclipse.amazonq.configuration.DefaultPluginStore; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; +import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil; import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthStateType; @@ -33,6 +33,7 @@ import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.SsoToken; +import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata; import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider; @@ -47,12 +48,15 @@ public final class DefaultLoginServiceTest { private static AmazonQLspServer mockAmazonQServer; private static PluginStore mockPluginStore; private static AuthStateManager mockAuthStateManager; - private static GetSsoTokenResult mockSsoTokenResult; private static MockedStatic mockedActivator; private static MockedStatic mockedAuthUtil; private static LoggingService mockLoggingService; private static AuthTokenService mockedAuthTokenService; private static AuthCredentialsService mockedAuthCredentialsService; + private static UpdateCredentialsPayload updateCredentialsPayload; + private static GetSsoTokenResult expectedSsoToken; + private static SsoToken ssoToken; + private static MockedStatic mockedCustomizationUtil; @BeforeEach public void setUp() { @@ -60,7 +64,6 @@ public void setUp() { mockAmazonQServer = mock(AmazonQLspServer.class); mockPluginStore = mock(DefaultPluginStore.class); mockAuthStateManager = mock(DefaultAuthStateManager.class); - mockSsoTokenResult = mock(GetSsoTokenResult.class); mockLoggingService = mock(LoggingService.class); mockedAuthTokenService = mock(AuthTokenService.class); mockedAuthCredentialsService = mock(AuthCredentialsService.class); @@ -68,26 +71,43 @@ public void setUp() { mockedActivator.when(Activator::getLogger).thenReturn(mockLoggingService); mockedAuthUtil = mockStatic(AuthUtil.class); mockedActivator.when(Activator::getLspProvider).thenReturn(mockLspProvider); + mockedCustomizationUtil = mockStatic(CustomizationUtil.class); + + updateCredentialsPayload = mock(UpdateCredentialsPayload.class); + when(updateCredentialsPayload.data()).thenReturn("data"); + when(updateCredentialsPayload.metadata()).thenReturn(new ConnectionMetadata()); + when(updateCredentialsPayload.encrypted()).thenReturn(true); + + ssoToken = mock(SsoToken.class); + when(ssoToken.id()).thenReturn("ssoTokenId"); + when(ssoToken.accessToken()).thenReturn("ssoAccessToken"); + + expectedSsoToken = mock(GetSsoTokenResult.class); + when(expectedSsoToken.getUpdateCredentialsPayloadHydratedWithStartUrl(any(String.class))) + .thenReturn(updateCredentialsPayload); + when(expectedSsoToken.ssoToken()).thenReturn(ssoToken); + when(expectedSsoToken.updateCredentialsParams()).thenReturn(updateCredentialsPayload); resetLoginService(); - when(mockLspProvider.getAmazonQServer()) - .thenReturn(CompletableFuture.completedFuture(mockAmazonQServer)); - when(mockAmazonQServer.getSsoToken(any())) - .thenReturn(CompletableFuture.completedFuture(mockSsoTokenResult)); + when(mockLspProvider.getAmazonQServer()).thenReturn(CompletableFuture.completedFuture(mockAmazonQServer)); + when(mockAmazonQServer.getSsoToken(any())).thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); } @AfterEach void tearDown() throws Exception { mockedActivator.close(); mockedAuthUtil.close(); + mockedCustomizationUtil.close(); } @Test void loginWhenAlreadyLoggedInValidation() { LoginType loginType = LoginType.BUILDER_ID; - LoginParams loginParams = createValidLoginParams(); - AuthState authState = createLoggedInAuthState(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.LOGGED_IN, LoginType.BUILDER_ID, loginParams, + Constants.AWS_BUILDER_ID_URL, "test-sso-token-id"); when(mockAuthStateManager.getAuthState()).thenReturn(authState); @@ -101,15 +121,15 @@ void loginWhenAlreadyLoggedInValidation() { @Test void loginBuilderIdSuccess() { LoginType loginType = LoginType.BUILDER_ID; - LoginParams loginParams = createValidLoginParams(); - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); - AuthState authState = createLoggedOutAuthState(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.LOGGED_OUT, LoginType.NONE, null, null, null); when(mockAuthStateManager.getAuthState()).thenReturn(authState); when(mockedAuthTokenService.getSsoToken(loginType, loginParams, true)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); - when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + when(mockedAuthCredentialsService.updateTokenCredentials(updateCredentialsPayload)) + .thenReturn(CompletableFuture.completedFuture(null)); CompletableFuture result = loginService.login(loginType, loginParams); @@ -117,22 +137,22 @@ void loginBuilderIdSuccess() { verify(mockLoggingService).info("Attempting to login..."); verify(mockLoggingService).info("Successfully logged in"); verify(mockedAuthTokenService).getSsoToken(loginType, loginParams, true); - verify(mockedAuthCredentialsService).updateTokenCredentials(expectedSsoToken.updateCredentialsParams()); + verify(mockedAuthCredentialsService).updateTokenCredentials(updateCredentialsPayload); verifyNoMoreInteractions(mockedAuthTokenService, mockedAuthCredentialsService); } @Test void loginIdcSuccess() { LoginType loginType = LoginType.IAM_IDENTITY_CENTER; - LoginParams loginParams = createValidLoginParams(); - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); - AuthState authState = createLoggedOutAuthState(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.LOGGED_OUT, LoginType.NONE, null, null, null); when(mockAuthStateManager.getAuthState()).thenReturn(authState); when(mockedAuthTokenService.getSsoToken(loginType, loginParams, true)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); - when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + when(mockedAuthCredentialsService.updateTokenCredentials(updateCredentialsPayload)) + .thenReturn(CompletableFuture.completedFuture(null)); CompletableFuture result = loginService.login(loginType, loginParams); @@ -140,13 +160,14 @@ void loginIdcSuccess() { verify(mockLoggingService).info("Attempting to login..."); verify(mockLoggingService).info("Successfully logged in"); verify(mockedAuthTokenService).getSsoToken(loginType, loginParams, true); - verify(mockedAuthCredentialsService).updateTokenCredentials(expectedSsoToken.updateCredentialsParams()); + verify(mockedAuthCredentialsService).updateTokenCredentials(updateCredentialsPayload); + verify(expectedSsoToken).getUpdateCredentialsPayloadHydratedWithStartUrl(any(String.class)); verifyNoMoreInteractions(mockedAuthTokenService, mockedAuthCredentialsService); } @Test void logoutWhenAlreadyLoggedOutValidation() { - AuthState authState = createLoggedOutAuthState(); + AuthState authState = createAuthState(AuthStateType.LOGGED_OUT, LoginType.NONE, null, null, null); when(mockAuthStateManager.getAuthState()).thenReturn(authState); CompletableFuture result = loginService.logout(); @@ -186,98 +207,101 @@ void logoutWithBlankSsoTokenIdValidation() { @Test void expireSuccess() { - when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(null, false))) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + UpdateCredentialsPayload updateCredentialsPayload = new UpdateCredentialsPayload(null, null, false); + when(mockedAuthCredentialsService.updateTokenCredentials(updateCredentialsPayload)) + .thenReturn(CompletableFuture.completedFuture(null)); CompletableFuture result = loginService.expire(); assertTrue(result.isDone()); verify(mockLoggingService).info("Attempting to expire credentials..."); - verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(null, false)); + verify(mockedAuthCredentialsService).updateTokenCredentials(updateCredentialsPayload); verify(mockAuthStateManager).toExpired(); verify(mockLoggingService).info("Successfully expired credentials"); verifyNoMoreInteractions(mockedAuthCredentialsService, mockAuthStateManager); } -// @Test -// void reAuthenticateBuilderIdNoLoginOnInvalidTokenSuccess() { -// AuthState authState = createExpiredBuilderAuthState(); -// GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); -// boolean loginOnInvalidToken = false; +// @Test +// void reAuthenticateBuilderIdNoLoginOnInvalidTokenSuccess() { +// AuthState authState = createExpiredBuilderAuthState(); +// GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); +// boolean loginOnInvalidToken = false; // -// when(mockAuthStateManager.getAuthState()).thenReturn(authState); -// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), false)) -// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); -// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) -// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); +// when(mockAuthStateManager.getAuthState()).thenReturn(authState); +// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), false)) +// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); +// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) +// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); // -// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); +// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); // -// assertTrue(result.isDone()); -// verify(mockLoggingService).info("Attempting to re-authenticate..."); -// verify(mockLoggingService).info("Successfully logged in"); -// verify(mockedAuthTokenService).getSsoToken(LoginType.BUILDER_ID, authState.loginParams(), false); -// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); -// verify(mockAuthStateManager).toLoggedIn(LoginType.BUILDER_ID, authState.loginParams(), expectedSsoToken.id()); -// } +// assertTrue(result.isDone()); +// verify(mockLoggingService).info("Attempting to re-authenticate..."); +// verify(mockLoggingService).info("Successfully logged in"); +// verify(mockedAuthTokenService).getSsoToken(LoginType.BUILDER_ID, authState.loginParams(), false); +// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); +// verify(mockAuthStateManager).toLoggedIn(LoginType.BUILDER_ID, authState.loginParams(), expectedSsoToken.id()); +// } // -// @Test -// void reAuthenticateBuilderIdWithLoginOnInvalidTokenSuccess() { -// AuthState authState = createExpiredBuilderAuthState(); -// GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); -// boolean loginOnInvalidToken = true; +// @Test +// void reAuthenticateBuilderIdWithLoginOnInvalidTokenSuccess() { +// AuthState authState = createExpiredBuilderAuthState(); +// GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); +// boolean loginOnInvalidToken = true; // -// when(mockAuthStateManager.getAuthState()).thenReturn(authState); -// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), true)) -// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); -// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) -// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); +// when(mockAuthStateManager.getAuthState()).thenReturn(authState); +// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), true)) +// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); +// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) +// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); // -// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); +// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); // -// assertTrue(result.isDone()); -// verify(mockLoggingService).info("Attempting to re-authenticate..."); -// verify(mockLoggingService).info("Successfully logged in"); -// verify(mockedAuthTokenService).getSsoToken(LoginType.BUILDER_ID, authState.loginParams(), true); -// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); -// verify(mockAuthStateManager).toLoggedIn(LoginType.BUILDER_ID, authState.loginParams(), expectedSsoToken.id()); -// } - -// @Test -// void reAuthenticateIdcNoLoginOnInvalidTokenSuccess() { -// AuthState authState = createExpiredIdcAuthState(); -// SsoToken expectedSsoToken = createSsoToken(); -// boolean loginOnInvalidToken = true; +// assertTrue(result.isDone()); +// verify(mockLoggingService).info("Attempting to re-authenticate..."); +// verify(mockLoggingService).info("Successfully logged in"); +// verify(mockedAuthTokenService).getSsoToken(LoginType.BUILDER_ID, authState.loginParams(), true); +// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); +// verify(mockAuthStateManager).toLoggedIn(LoginType.BUILDER_ID, authState.loginParams(), expectedSsoToken.id()); +// } + +// @Test +// void reAuthenticateIdcNoLoginOnInvalidTokenSuccess() { +// AuthState authState = createExpiredIdcAuthState(); +// SsoToken expectedSsoToken = createSsoToken(); +// boolean loginOnInvalidToken = true; // -// when(mockAuthStateManager.getAuthState()).thenReturn(authState); -// when(mockSsoTokenResult.ssoToken()).thenReturn(expectedSsoToken); -// when(mockEncryptionManager.decrypt(expectedSsoToken.accessToken())).thenReturn("-decryptedAccessToken-"); -// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), true)) -// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); -// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) -// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); +// when(mockAuthStateManager.getAuthState()).thenReturn(authState); +// when(mockSsoTokenResult.ssoToken()).thenReturn(expectedSsoToken); +// when(mockEncryptionManager.decrypt(expectedSsoToken.accessToken())).thenReturn("-decryptedAccessToken-"); +// when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), true)) +// .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); +// when(mockedAuthCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true))) +// .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); // -// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); +// CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); // -// assertTrue(result.isDone()); -// verify(mockLoggingService).info("Attempting to re-authenticate..."); -// verify(mockLoggingService).info("Successfully logged in"); -// verify(mockedAuthTokenService).getSsoToken(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), true); -// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); -// verify(mockAuthStateManager).toLoggedIn(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), expectedSsoToken.id()); -// } +// assertTrue(result.isDone()); +// verify(mockLoggingService).info("Attempting to re-authenticate..."); +// verify(mockLoggingService).info("Successfully logged in"); +// verify(mockedAuthTokenService).getSsoToken(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), true); +// verify(mockedAuthCredentialsService).updateTokenCredentials(new UpdateCredentialsPayload(expectedSsoToken.accessToken(), true)); +// verify(mockAuthStateManager).toLoggedIn(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), expectedSsoToken.id()); +// } @Test void reAuthenticateIdcWithLoginOnInvalidTokenSuccess() { - AuthState authState = createExpiredIdcAuthState(); - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.EXPIRED, LoginType.IAM_IDENTITY_CENTER, loginParams, + Constants.AWS_BUILDER_ID_URL, "test-sso-token-id"); boolean loginOnInvalidToken = false; when(mockAuthStateManager.getAuthState()).thenReturn(authState); when(mockedAuthTokenService.getSsoToken(authState.loginType(), authState.loginParams(), false)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); CompletableFuture result = loginService.reAuthenticate(loginOnInvalidToken); @@ -286,12 +310,13 @@ void reAuthenticateIdcWithLoginOnInvalidTokenSuccess() { verify(mockLoggingService).info("Successfully logged in"); verify(mockedAuthTokenService).getSsoToken(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), false); verify(mockedAuthCredentialsService).updateTokenCredentials(expectedSsoToken.updateCredentialsParams()); - verify(mockAuthStateManager).toLoggedIn(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), expectedSsoToken.ssoToken().id()); + verify(mockAuthStateManager).toLoggedIn(LoginType.IAM_IDENTITY_CENTER, authState.loginParams(), + expectedSsoToken.ssoToken().id()); } @Test void reAuthenticateWhenLoggedOutValidation() { - AuthState authState = createLoggedOutAuthState(); + AuthState authState = createAuthState(AuthStateType.LOGGED_OUT, LoginType.NONE, null, null, null); when(mockAuthStateManager.getAuthState()).thenReturn(authState); CompletableFuture result = loginService.reAuthenticate(true); @@ -302,16 +327,19 @@ void reAuthenticateWhenLoggedOutValidation() { } @Test - void processLoginBuilderIdNoLoginOnInvalidTokenSuccess() throws Exception { + void processLoginBuilderIdWithLoginOnInvalidTokenSuccess() throws Exception { LoginType loginType = LoginType.BUILDER_ID; - LoginParams loginParams = createValidLoginParams(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.LOGGED_IN, LoginType.BUILDER_ID, loginParams, + Constants.AWS_BUILDER_ID_URL, "test-sso-token-id"); + boolean loginOnInvalidToken = false; - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); when(mockedAuthTokenService.getSsoToken(loginType, loginParams, loginOnInvalidToken)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); invokeProcessLogin(loginType, loginParams, loginOnInvalidToken); @@ -323,16 +351,19 @@ void processLoginBuilderIdNoLoginOnInvalidTokenSuccess() throws Exception { } @Test - void processLoginBuilderIdWithLoginOnInvalidTokenSuccess() throws Exception { + void processLoginBuilderIdNoLoginOnInvalidTokenSuccess() throws Exception { LoginType loginType = LoginType.BUILDER_ID; - LoginParams loginParams = createValidLoginParams(); - boolean loginOnInvalidToken = true; - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); + AuthState authState = createAuthState(AuthStateType.LOGGED_IN, LoginType.BUILDER_ID, loginParams, + Constants.AWS_BUILDER_ID_URL, "test-sso-token-id"); + + boolean loginOnInvalidToken = false; when(mockedAuthTokenService.getSsoToken(loginType, loginParams, loginOnInvalidToken)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); invokeProcessLogin(loginType, loginParams, loginOnInvalidToken); @@ -341,19 +372,20 @@ void processLoginBuilderIdWithLoginOnInvalidTokenSuccess() throws Exception { verify(mockedAuthCredentialsService).updateTokenCredentials(expectedSsoToken.updateCredentialsParams()); verify(mockAuthStateManager).toLoggedIn(loginType, loginParams, expectedSsoToken.ssoToken().id()); verify(mockLoggingService).info("Successfully logged in"); + } @Test void processLoginIdcNoLoginOnInvalidTokenSuccess() throws Exception { LoginType loginType = LoginType.IAM_IDENTITY_CENTER; - LoginParams loginParams = createValidLoginParams(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); boolean loginOnInvalidToken = false; - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); when(mockedAuthTokenService.getSsoToken(loginType, loginParams, loginOnInvalidToken)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); invokeProcessLogin(loginType, loginParams, loginOnInvalidToken); @@ -367,14 +399,14 @@ void processLoginIdcNoLoginOnInvalidTokenSuccess() throws Exception { @Test void processLoginIdcWithLoginOnInvalidTokenSuccess() throws Exception { LoginType loginType = LoginType.IAM_IDENTITY_CENTER; - LoginParams loginParams = createValidLoginParams(); + LoginIdcParams idcParams = createLoginIdcParams("test-region", "test-url"); + LoginParams loginParams = createLoginParams(idcParams); boolean loginOnInvalidToken = true; - GetSsoTokenResult expectedSsoToken = createSsoTokenResult(); when(mockedAuthTokenService.getSsoToken(loginType, loginParams, loginOnInvalidToken)) - .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); + .thenReturn(CompletableFuture.completedFuture(expectedSsoToken)); when(mockedAuthCredentialsService.updateTokenCredentials(expectedSsoToken.updateCredentialsParams())) - .thenReturn(CompletableFuture.completedFuture(new ResponseMessage())); + .thenReturn(CompletableFuture.completedFuture(null)); invokeProcessLogin(loginType, loginParams, loginOnInvalidToken); @@ -385,55 +417,48 @@ void processLoginIdcWithLoginOnInvalidTokenSuccess() throws Exception { verify(mockLoggingService).info("Successfully logged in"); } - private void resetLoginService() { - loginService = new DefaultLoginService.Builder() - .withLspProvider(mockLspProvider) - .withPluginStore(mockPluginStore) - .withAuthStateManager(mockAuthStateManager) - .withAuthCredentialsService(mockedAuthCredentialsService) - .withAuthTokenService(mockedAuthTokenService) - .build(); - loginService = spy(loginService); - } - - private AuthState createLoggedInAuthState() { - String ssoTokenId = "ssoTokenId"; - LoginParams loginParams = createValidLoginParams(); - return new AuthState(AuthStateType.LOGGED_IN, LoginType.BUILDER_ID, loginParams, Constants.AWS_BUILDER_ID_URL, ssoTokenId); + private LoginParams createLoginParams(final LoginIdcParams idcParams) { + LoginParams loginParams = mock(LoginParams.class); + when(loginParams.getLoginIdcParams()).thenReturn(idcParams); + return loginParams; } - private AuthState createExpiredBuilderAuthState() { - String ssoTokenId = "ssoTokenId"; - LoginParams loginParams = createValidLoginParams(); - return new AuthState(AuthStateType.EXPIRED, LoginType.BUILDER_ID, loginParams, Constants.AWS_BUILDER_ID_URL, ssoTokenId); + private LoginIdcParams createLoginIdcParams(final String region, final String url) { + LoginIdcParams idcParams = mock(LoginIdcParams.class); + when(idcParams.getRegion()).thenReturn(region); + when(idcParams.getUrl()).thenReturn(url); + return idcParams; } - private AuthState createExpiredIdcAuthState() { - String ssoTokenId = "ssoTokenId"; - LoginParams loginParams = createValidLoginParams(); - return new AuthState(AuthStateType.EXPIRED, LoginType.IAM_IDENTITY_CENTER, loginParams, Constants.AWS_BUILDER_ID_URL, ssoTokenId); + private void resetLoginService() { + loginService = new DefaultLoginService.Builder() + .withLspProvider(mockLspProvider) + .withPluginStore(mockPluginStore) + .withAuthStateManager(mockAuthStateManager) + .withAuthCredentialsService(mockedAuthCredentialsService) + .withAuthTokenService(mockedAuthTokenService) + .build(); + loginService = spy(loginService); } - private AuthState createLoggedOutAuthState() { - return new AuthState(AuthStateType.LOGGED_OUT, LoginType.NONE, null, null, null); - } + private AuthState createAuthState(final AuthStateType authStateType, final LoginType loginType, + final LoginParams loginParams, final String issuerUrl, final String ssoTokenId) { + AuthState authState = mock(AuthState.class); + when(authState.authStateType()).thenReturn(authStateType); + when(authState.loginType()).thenReturn(loginType); + when(authState.loginParams()).thenReturn(loginParams); + when(authState.issuerUrl()).thenReturn(issuerUrl); + when(authState.ssoTokenId()).thenReturn(ssoTokenId); - private LoginParams createValidLoginParams() { - LoginParams loginParams = new LoginParams(); - LoginIdcParams idcParams = new LoginIdcParams(); - idcParams.setRegion("test-region"); - idcParams.setUrl("https://example.com"); - loginParams.setLoginIdcParams(idcParams); - return loginParams; - } + when(authState.isLoggedIn()).thenReturn(authStateType.equals(AuthStateType.LOGGED_IN)); + when(authState.isLoggedOut()).thenReturn(authStateType.equals(AuthStateType.LOGGED_OUT)); + when(authState.isExpired()).thenReturn(authStateType.equals(AuthStateType.EXPIRED)); - private GetSsoTokenResult createSsoTokenResult() { - String id = "ssoTokenId"; - String accessToken = "ssoAccessToken"; - return new GetSsoTokenResult(new SsoToken(id, accessToken), new UpdateCredentialsPayload(accessToken, false)); + return authState; } - private void invokeProcessLogin(final LoginType loginType, final LoginParams loginParams, final boolean loginOnInvalidToken) throws Exception { + private void invokeProcessLogin(final LoginType loginType, final LoginParams loginParams, + final boolean loginOnInvalidToken) throws Exception { Object processLoginFuture = loginService.processLogin(loginType, loginParams, loginOnInvalidToken); assertTrue(processLoginFuture instanceof CompletableFuture, "Return value should be CompletableFuture"); @@ -441,4 +466,5 @@ private void invokeProcessLogin(final LoginType loginType, final LoginParams log Object result = future.get(); assertNull(result); } + } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcherTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcherTest.java index 34c5c90df..02eee9fff 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcherTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/manager/fetcher/RemoteLspFetcherTest.java @@ -1,3 +1,6 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + package software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProviderTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProviderTest.java index e1ddc5bb8..234fd1658 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProviderTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/providers/browser/AmazonQBrowserProviderTest.java @@ -46,7 +46,7 @@ public void getBrowserStyle(final PluginPlatform platform, final int expectedSty staticDisplay.when(Display::getDefault).thenReturn(mockDisplay); doNothing().when(mockDisplay).asyncExec(any(Runnable.class)); - browserProvider = new AmazonQBrowserProvider(platform); + browserProvider = AmazonQBrowserProvider.builder().withPluginPlatform(platform).build(); assertEquals(expectedStyle, browserProvider.getBrowserStyle()); } } @@ -64,10 +64,10 @@ void checkWebViewCompatibility(final PluginPlatform platform, final String brows staticDisplay.when(Display::getDefault).thenReturn(mockDisplay); doNothing().when(mockDisplay).asyncExec(any(Runnable.class)); - browserProvider = new AmazonQBrowserProvider(platform); + browserProvider = AmazonQBrowserProvider.builder().withPluginPlatform(platform).build(); assertFalse(browserProvider.hasWebViewDependency()); - browserProvider.checkWebViewCompatibility(browserType); + browserProvider.checkWebViewCompatibility(browserType, false); assertEquals(expectedResult, browserProvider.hasWebViewDependency()); } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java index 749762a7a..7e7b88698 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/views/router/ViewRouterTest.java @@ -3,12 +3,14 @@ package software.aws.toolkits.eclipse.amazonq.views.router; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -23,6 +25,7 @@ import software.aws.toolkits.eclipse.amazonq.broker.events.AmazonQViewType; import software.aws.toolkits.eclipse.amazonq.broker.events.BrowserCompatibilityState; import software.aws.toolkits.eclipse.amazonq.broker.events.ChatWebViewAssetState; +import software.aws.toolkits.eclipse.amazonq.broker.events.QDeveloperProfileState; import software.aws.toolkits.eclipse.amazonq.broker.events.ToolkitLoginWebViewAssetState; import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; @@ -37,6 +40,7 @@ public final class ViewRouterTest { private Observable browserCompatibilityStateObservable; private Observable chatWebViewAssetStateObservable; private Observable toolkitLoginWebViewAssetStateObservable; + private Observable qDeveloperProfileStateObservable; private ViewRouter viewRouter; private EventBroker eventBrokerMock; @@ -56,6 +60,7 @@ void setupBeforeEach() { browserCompatibilityStateObservable = publishSubject.ofType(BrowserCompatibilityState.class); chatWebViewAssetStateObservable = publishSubject.ofType(ChatWebViewAssetState.class); toolkitLoginWebViewAssetStateObservable = publishSubject.ofType(ToolkitLoginWebViewAssetState.class); + qDeveloperProfileStateObservable = publishSubject.ofType(QDeveloperProfileState.class); eventBrokerMock = activatorStaticMockExtension.getMock(EventBroker.class); @@ -63,7 +68,8 @@ void setupBeforeEach() { .withLspStateObservable(lspStateObservable) .withBrowserCompatibilityStateObservable(browserCompatibilityStateObservable) .withChatWebViewAssetStateObservable(chatWebViewAssetStateObservable) - .withToolkitLoginWebViewAssetStateObservable(toolkitLoginWebViewAssetStateObservable).build(); + .withToolkitLoginWebViewAssetStateObservable(toolkitLoginWebViewAssetStateObservable) + .withQDeveloperProfileStateObservable(qDeveloperProfileStateObservable).build(); } @AfterEach @@ -83,10 +89,30 @@ void testActiveViewResolutionBasedOnPluginState(final AmazonQLspState lspState, publishSubject.onNext(browserCompatibilityState); publishSubject.onNext(chatWebViewAssetState); publishSubject.onNext(toolkitLoginWebViewAssetState); + publishSubject.onNext(QDeveloperProfileState.AVAILABLE); // does not affect view selection verify(eventBrokerMock).post(AmazonQViewType.class, expectedActiveViewType); } + @Test + void testDuplicateViewIdPublishedWhenDeveloperProfileSelected() { + publishSubject.onNext(getAuthStateObject(AuthStateType.LOGGED_IN)); + publishSubject.onNext(AmazonQLspState.ACTIVE); + publishSubject.onNext(BrowserCompatibilityState.COMPATIBLE); + publishSubject.onNext(ChatWebViewAssetState.RESOLVED); + publishSubject.onNext(ToolkitLoginWebViewAssetState.RESOLVED); + publishSubject.onNext(QDeveloperProfileState.AVAILABLE); + + publishSubject.onNext(getAuthStateObject(AuthStateType.LOGGED_IN)); + publishSubject.onNext(AmazonQLspState.ACTIVE); + publishSubject.onNext(BrowserCompatibilityState.COMPATIBLE); + publishSubject.onNext(ChatWebViewAssetState.RESOLVED); + publishSubject.onNext(ToolkitLoginWebViewAssetState.RESOLVED); + publishSubject.onNext(QDeveloperProfileState.SELECTED); + + verify(eventBrokerMock, times(2)).post(AmazonQViewType.class, AmazonQViewType.CHAT_VIEW); + } + private static Stream provideStateSource() { return Stream.of( Arguments.of(AmazonQLspState.ACTIVE, getAuthStateObject(AuthStateType.LOGGED_IN), diff --git a/plugin/webview/src/ideClient.ts b/plugin/webview/src/ideClient.ts index 6058f4763..54be9d2de 100644 --- a/plugin/webview/src/ideClient.ts +++ b/plugin/webview/src/ideClient.ts @@ -1,8 +1,9 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { profile } from "console"; import {Store} from "vuex"; -import {IdcInfo, Region, Stage, State, BrowserSetupData, AwsBearerTokenConnection} from "./model"; +import {IdcInfo, Region, Stage, State, BrowserSetupData, AwsBearerTokenConnection, Profile} from "./model"; export class IdeClient { constructor(private readonly store: Store) {} @@ -16,7 +17,7 @@ export class IdeClient { this.updateLastLoginIdcInfo(state.idcInfo) this.store.commit("setCancellable", state.cancellable) this.store.commit("setFeature", state.feature) - + this.store.commit('setProfiles', state.profiles); const existConnections = state.existConnections.map(it => { return { sessionName: it.sessionName, @@ -29,6 +30,14 @@ export class IdeClient { this.store.commit("setExistingConnections", existConnections) this.updateAuthorization(undefined) + this.updateRedirectUrl(undefined) + } + + handleProfiles(profilesData: { profiles: Profile[] }) { + this.store.commit('setStage', 'PROFILE_SELECT') + console.debug("received profile data") + const availableProfiles: Profile[] = profilesData.profiles; + this.store.commit('setProfiles', availableProfiles); } updateAuthorization(code: string | undefined) { @@ -40,8 +49,12 @@ export class IdeClient { this.store.commit('setLastLoginIdcInfo', idcInfo) } + updateRedirectUrl(redirectUrl: string | undefined) { + this.store.commit('setRedirectUrl', redirectUrl) + } + reset() { - this.store.commit('reset') + this.store.commit('setStage', 'START') } cancelLogin(): void { diff --git a/plugin/webview/src/model.ts b/plugin/webview/src/model.ts index 424628e01..dde2aa990 100644 --- a/plugin/webview/src/model.ts +++ b/plugin/webview/src/model.ts @@ -7,7 +7,8 @@ export type BrowserSetupData = { idcInfo: IdcInfo, cancellable: boolean, feature: string, - existConnections: AwsBearerTokenConnection[] + existConnections: AwsBearerTokenConnection[], + profiles: Profile[] } // plugin interface [AwsBearerTokenConnection] @@ -26,7 +27,8 @@ export type Stage = 'CONNECTED' | 'AUTHENTICATING' | 'AWS_PROFILE' | - 'REAUTH' + 'REAUTH' | + 'PROFILE_SELECT' export type Feature = 'Q' | 'codecatalyst' | 'awsExplorer' @@ -47,10 +49,13 @@ export interface State { stage: Stage, ssoRegions: Region[], authorizationCode: string | undefined, + redirectUrl: string | undefined, lastLoginIdcInfo: IdcInfo, feature: Feature, cancellable: boolean, - existingConnections: AwsBearerTokenConnection[] + existingConnections: AwsBearerTokenConnection[], + profiles: Profile[], + selectedProfile: Profile | null } export enum LoginIdentifier { @@ -67,6 +72,15 @@ export interface LoginOption { requiresBrowser(): boolean } +export interface Profile { + accountId: string, + arn: string, + name: string, + identityDetails: { + region: string + } +} + export class LongLivedIAM implements LoginOption { id: LoginIdentifier = LoginIdentifier.IAM_CREDENTIAL diff --git a/plugin/webview/src/q-ui/components/authenticating.vue b/plugin/webview/src/q-ui/components/authenticating.vue index a88ba6142..13548d44c 100644 --- a/plugin/webview/src/q-ui/components/authenticating.vue +++ b/plugin/webview/src/q-ui/components/authenticating.vue @@ -4,6 +4,7 @@