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/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index 24910ff6e..821f4ad22 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -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", 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/pom.xml b/plugin/pom.xml index 4a938dd21..f03fa71fe 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -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 @@ -64,7 +64,7 @@ commons-codec commons-codec - 1.17.1 + 1.17.2 software.amazon.awssdk @@ -123,10 +123,10 @@ test - io.github.java-diff-utils - java-diff-utils - 4.15 - + io.github.java-diff-utils + java-diff-utils + 4.15 + @@ -166,7 +166,7 @@ maven-dependency-plugin - 3.8.0 + 3.8.1 copy-dependencies @@ -257,7 +257,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.1 + 3.5.3 test @@ -273,7 +273,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 compiletests @@ -287,12 +287,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 +314,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..13977b731 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatAsyncResultManager.java @@ -0,0 +1,85 @@ +// 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(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); + } + } + + + 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..5ed109c06 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java @@ -4,10 +4,16 @@ 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; @@ -16,25 +22,21 @@ 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 +51,202 @@ * 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 = 0; + 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: + 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 +254,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 +271,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 +291,113 @@ public void run() { return range.get().map(CursorState::new); } - private CompletableFuture sendEncryptedChatMessage(final String tabId, + 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 +421,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 +439,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 +446,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; + } + } + } + + // normal partial processing + Object lock = partialResultLocks.get(token); + if (lock == null) { + return; + } - ChatUIInboundCommand chatUIInboundCommand = new ChatUIInboundCommand( - command, tabId, partialChatResult, true); + synchronized (lock) { + if (partialResultLocks.get(token) == null || Boolean.TRUE.equals(finalResultProcessed.get(token))) { + return; + } - sendMessageToChatUI(chatUIInboundCommand); + 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 +567,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 +619,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..5b1570266 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); @@ -99,13 +67,17 @@ private String getCssForDarkTheme() { // Card themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor); + themeMap.put(QChatCssVariable.CardBackgroundAlternate, rgba(0, 0, 0, 0.5)); themeMap.put(QChatCssVariable.LineHeight, "1.25em"); - return getCss(themeMap); + // Input + themeMap.put(QChatCssVariable.InputBackground, rgb(60, 60, 60)); + + return themeMap; } - private String getCssForLightTheme() { + private Map getLightThemeMap() { Map themeMap = new HashMap<>(); String defaultTextColor = rgb(10, 10, 10); @@ -151,13 +123,17 @@ private String getCssForLightTheme() { // Card themeMap.put(QChatCssVariable.CardBackground, cardBackgroundColor); + themeMap.put(QChatCssVariable.CardBackgroundAlternate, rgba(0, 0, 0, 0.5)); themeMap.put(QChatCssVariable.LineHeight, "1.25em"); - return getCss(themeMap); + // Input + themeMap.put(QChatCssVariable.InputBackground, rgb(255, 255, 255)); + + 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 +141,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 +149,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..51f53a3f3 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 @@ -45,9 +45,13 @@ 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"); 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..ae5904e07 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java @@ -0,0 +1,322 @@ +// 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.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.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 { + return deserializeProfile(json); + } 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 QDeveloperProfile deserializeProfile(final String json) throws JsonProcessingException { + return OBJECT_MAPPER.readValue(json, QDeveloperProfile.class); + } + + private String serializeProfile(final QDeveloperProfile developerProfile) throws JsonProcessingException { + 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()) && StringUtils.isNotBlank(profile.getArn()) + && StringUtils.isNotBlank(profile.getAccountId()); + } + + 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); + 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..0d635d6d0 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatDiffManager.java @@ -248,6 +248,10 @@ void restoreState() { } + void endSession() { + task = null; + } + private void setColorPalette(final boolean isDark) { this.annotationAdded = "diffAnnotation.added"; this.annotationDeleted = "diffAnnotation.deleted"; 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..e23b9f817 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 { @@ -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()) { @@ -273,10 +275,13 @@ private synchronized void endSession() { } catch (Exception e) { Activator.getLogger().error("FAILURE ON EMISSION:", e); } - uiManager.closePrompt(); + uiManager.endSession(); + diffManager.endSession(); cleanupSessionState(); setState(SessionState.INACTIVE); Activator.getLogger().info("Inline chat session ended."); + }).thenRun(() -> { + task = null; }); } 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..489810ad6 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java @@ -321,6 +321,11 @@ void closePrompt() { }); } + void endSession() { + closePrompt(); + task = null; + } + private void removeCurrentPaintListener() { if (viewer == null) { return; 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..96242ac9e 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,79 @@ 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.runtime.CoreException; +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 +83,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 +134,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,15 +199,38 @@ 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 { + 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 +256,292 @@ 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(); + } } 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..6ed643d20 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; @@ -45,6 +47,10 @@ protected IStatus run(final IProgressMonitor monitor) { Display.getDefault().asyncExec(() -> { AmazonQToolbarActions.getInstance(); }); + AmazonQBrowserProvider.getInstance().publishBrowserCompatibilityState(); + Activator.getEventBroker().post(QDeveloperProfileState.class, + QDeveloperProfileState.NOT_APPLICABLE); + Activator.getLspProvider().getAmazonQServer().thenAcceptAsync(server -> { try { schedulePostStartupJobs(); @@ -84,7 +90,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/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..4457b56e9 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"; @@ -32,7 +32,7 @@ private LspConstants() { 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/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..16e5f6da8 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -10,7 +10,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 +19,12 @@ 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; public AmazonQChatWebview() { super(); - chatStateManager = ChatStateManager.getInstance(); chatCommunicationManager = ChatCommunicationManager.getInstance(); webViewAssetProvider = new ChatWebViewAssetProvider(); webViewAssetProvider.initialize(); @@ -37,20 +33,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 +51,39 @@ 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..135b5d1ce --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ChangeProfileDialog.java @@ -0,0 +1,422 @@ +// 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); + + 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)); + 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("\u23AF"); // 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..431df3100 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()))); } 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..21a4313eb 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,14 +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); default: Activator.getLogger() .error("Unexpected command received from Amazon Q Login: " + parsedCommand.getCommand()); 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..a9a578720 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(); + changeProfileDialogContributionItem = new ChangeProfileDialogContributionItem(); + openPreferencesAction = new OpenPreferencesAction(); } 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..129699d0d --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/model/Configuration.java @@ -0,0 +1,78 @@ +// 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(":"); + + Activator.getLogger().info(chunks[4]); + + // 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..43891d42a 100644 --- a/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java +++ b/plugin/src/software/aws/toolkits/eclipse/workspace/WorkspaceChangeListener.java @@ -1,8 +1,12 @@ +// 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.concurrent.atomic.AtomicReference; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; @@ -22,20 +26,17 @@ 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 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 +48,105 @@ 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) { + if (!isIndexingEnabled()) { + return; + } + try { + FileChanges changes = fileChangeTracker.trackChanges(event.getDelta()); + notifyLspServer(changes); + } catch (Exception e) { + Activator.getLogger().error("Error processing workspace changes", e); + } + } + + private boolean isIndexingEnabled() { + return Activator.getDefault().getPreferenceStore().getBoolean(AmazonQPreferencePage.WORKSPACE_INDEX); + } + + 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(); - IResourceDelta delta = event.getDelta(); + delta.accept(resourceDelta -> { + if (resourceDelta.getResource().getType() != IResource.FILE) { + return true; + } - List createdFiles = new ArrayList<>(); - List deletedFiles = new ArrayList<>(); - List renamedFiles = new ArrayList<>(); + processResourceDelta(resourceDelta, changes); + return true; + }); + 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..bbc9a7073 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,613 @@ 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..d7791ca2c 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 @@ -14,7 +14,7 @@ public class QChatCssVariableTest { @Test void testEnumValues() { - assertEquals(28, QChatCssVariable.values().length); + assertEquals(30, QChatCssVariable.values().length); } @Test 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/DefaultAuthStateManagerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java index ee94f1315..706a5968e 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java @@ -41,17 +41,28 @@ class DefaultAuthStateManagerTest { private DefaultAuthStateManager authStateManager; private LoginParams loginParams; private AutoCloseable closeable; + + // Constants for test values + private static final String TEST_URL = "https://example.com"; + private static final String TEST_TOKEN_ID = "testToken"; + private static final String SSO_TOKEN_ID = "ssoTokenId"; @BeforeEach void setUp() { closeable = MockitoAnnotations.openMocks(this); - assertNotNull(pluginStore, "PluginStore mock should not be null"); - + + // Initialize manager authStateManager = new DefaultAuthStateManager(pluginStore); + + // Setup common test data + setupLoginParams(); + } + + private void setupLoginParams() { loginParams = new LoginParams(); LoginIdcParams idcParams = new LoginIdcParams(); - idcParams.setUrl("https://example.com"); + idcParams.setUrl(TEST_URL); loginParams.setLoginIdcParams(idcParams); } 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..17565b9e2 100644 --- a/plugin/webview/src/q-ui/components/authenticating.vue +++ b/plugin/webview/src/q-ui/components/authenticating.vue @@ -4,6 +4,7 @@