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 069acb63..116f2b6e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java @@ -258,6 +258,50 @@ public void sendMessageToChatServer(final Command command, final ChatMessage mes Activator.getLogger().error("Error processing mcpServerClick: " + e); } break; + case LIST_RULES: + try { + Object listRulesResponse = amazonQLspServer.listRules(message.getData()).get(); + var listRulesCommand = ChatUIInboundCommand.createCommand("aws/chat/listRules", + listRulesResponse); + Activator.getEventBroker().post(ChatUIInboundCommand.class, listRulesCommand); + } catch (Exception e) { + Activator.getLogger().error("Error processing listRules: " + e); + } + break; + case RULE_CLICK: + try { + Object ruleClickResponse = amazonQLspServer.ruleClick(message.getData()).get(); + var ruleClickCommand = ChatUIInboundCommand.createCommand("aws/chat/ruleClick", + ruleClickResponse); + Activator.getEventBroker().post(ChatUIInboundCommand.class, ruleClickCommand); + } catch (Exception e) { + Activator.getLogger().error("Error processing ruleClick: " + e); + } + break; + case PINNED_CONTEXT_ADD: + try { + Activator.getLogger().info("Pinned Context: Processing add command with data: " + message.getData()); + amazonQLspServer.pinnedContextAdd(message.getData()); + } catch (Exception e) { + Activator.getLogger().error("Pinned Context: Error processing add command: " + e); + } + break; + case PINNED_CONTEXT_REMOVE: + try { + Activator.getLogger().info("Pinned Context: Processing remove command"); + amazonQLspServer.pinnedContextRemove(message.getData()); + } catch (Exception e) { + Activator.getLogger().error("Pinned Context: Error processing remove command: " + e); + } + break; + case SEND_PINNED_CONTEXT: + try { + Activator.getLogger().info("Pinned Context: Processing send command"); + amazonQLspServer.sendPinnedContext(message.getData()); + } catch (Exception e) { + Activator.getLogger().error("Pinned Context: Error processing send command: " + e); + } + break; default: throw new AmazonQPluginException("Unexpected command received from Chat UI: " + command.toString()); } 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 a9fa44ed..521530ca 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 @@ -15,7 +15,12 @@ public enum ChatUIInboundCommandName { GenericCommand("genericCommand"), ChatOptionsUpdate("aws/chat/chatOptionsUpdate"), ListMcpServers("aws/chat/listMcpServers"), - McpServerClick("aws/chat/mcpServerClick"); + McpServerClick("aws/chat/mcpServerClick"), + ListRules("aws/chat/listRules"), + RuleClick("aws/chat/ruleClick"), + PinnedContextAdd("aws/chat/pinnedContextAdd"), + PinnedContextRemove("aws/chat/pinnedContextRemove"), + SendPinnedContext("aws/chat/sendPinnedContext"); private final String value; 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 d50d5bc7..5493c9a4 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClient.java @@ -61,4 +61,10 @@ public interface AmazonQLspClient extends LanguageClient { @JsonNotification("aws/didCreateDirectory") void didCreateDirectory(Object params); + @JsonNotification("aws/chat/sendPinnedContext") + void sendPinnedContext(Object params); + + @JsonNotification("aws/chat/activeEditorChanged") + void activeEditorChanged(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 8f44c782..56fc00b3 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java @@ -82,6 +82,7 @@ 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; +import software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils; import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage; import software.aws.toolkits.eclipse.amazonq.telemetry.service.DefaultTelemetryService; import software.aws.toolkits.eclipse.amazonq.util.Constants; @@ -575,4 +576,45 @@ private boolean isUriInWorkspace(final String uri) { return false; } } + + @Override + public final void sendPinnedContext(final Object params) { + Activator.getLogger().info("Pinned Context: sendPinnedContext called with params: " + params); + + Object updatedParams = params; + + // Use existing utility to get active text editor and file URI + ITextEditor activeEditor = QEclipseEditorUtils.getActiveTextEditor(); + if (activeEditor != null) { + Optional fileUri = QEclipseEditorUtils.getOpenFileUri(activeEditor.getEditorInput()); + if (fileUri.isPresent()) { + Map textDocument = new HashMap<>(); + textDocument.put("uri", fileUri.get()); + + if (params instanceof Map) { + @SuppressWarnings("unchecked") + Map paramsMap = new HashMap<>((Map) params); + paramsMap.put("textDocument", textDocument); + updatedParams = paramsMap; + } else { + Map wrappedParams = new HashMap<>(); + wrappedParams.put("params", params); + wrappedParams.put("textDocument", textDocument); + updatedParams = wrappedParams; + } + } + } + + var sendPinnedContextCommand = new ChatUIInboundCommand("aws/chat/sendPinnedContext", null, updatedParams, + false, null); + Activator.getEventBroker().post(ChatUIInboundCommand.class, sendPinnedContextCommand); + } + + @Override + public final void activeEditorChanged(final Object params) { + // This notification is sent from Eclipse to server when editor changes + // In Phase 3, this will be handled by the ActiveEditorChangeListener + // For now, just log it + Activator.getLogger().info("Active editor changed notification received: " + params); + } } 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 06c0572d..3020780e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServer.java @@ -129,4 +129,22 @@ CompletableFuture> getConfi @JsonRequest("aws/chat/mcpServerClick") CompletableFuture mcpServerClick(Object params); + + @JsonRequest("aws/chat/listRules") + CompletableFuture listRules(Object params); + + @JsonRequest("aws/chat/ruleClick") + CompletableFuture ruleClick(Object params); + + @JsonNotification("aws/chat/pinnedContextAdd") + void pinnedContextAdd(Object params); + + @JsonNotification("aws/chat/pinnedContextRemove") + void pinnedContextRemove(Object params); + + @JsonNotification("aws/chat/sendPinnedContext") + void sendPinnedContext(Object params); + + @JsonNotification("aws/chat/activeEditorChanged") + void activeEditorChanged(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 51d7f74e..4a8a2c32 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspServerBuilder.java @@ -56,6 +56,7 @@ private Map getInitializationOptions(final ClientMetadata metada qOptions.put("developerProfiles", true); qOptions.put("customizationsWithMetadata", true); qOptions.put("mcp", true); + qOptions.put("pinnedContextEnabled", true); awsClientCapabilities.put("q", qOptions); Map window = new HashMap<>(); window.put("showSaveFileDialog", true); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/editor/ActiveEditorChangeListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/editor/ActiveEditorChangeListener.java new file mode 100644 index 00000000..7117dc8a --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/editor/ActiveEditorChangeListener.java @@ -0,0 +1,190 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.lsp.editor; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IWorkbenchPartReference; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.ITextEditor; + +import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.util.QEclipseEditorUtils; + +/** + * Listens for active editor changes and notifies the language server + * with debouncing to avoid excessive notifications. + */ +public final class ActiveEditorChangeListener implements IPartListener2 { + private static final long DEBOUNCE_DELAY_MS = 100L; + + private final AmazonQLspServer languageServer; + private final ScheduledExecutorService executor; + private ScheduledFuture debounceTask; + + public ActiveEditorChangeListener(final AmazonQLspServer languageServer, final ScheduledExecutorService executor) { + this.languageServer = languageServer; + this.executor = executor; + } + + @Override + public void partActivated(final IWorkbenchPartReference partRef) { + if (partRef.getPart(false) instanceof ITextEditor) { + handleEditorChange((ITextEditor) partRef.getPart(false)); + } + } + + @Override + public void partBroughtToTop(final IWorkbenchPartReference partRef) { + if (partRef.getPart(false) instanceof ITextEditor) { + handleEditorChange((ITextEditor) partRef.getPart(false)); + } + } + + @Override + public void partClosed(final IWorkbenchPartReference partRef) { + // When editor is closed, send notification with null values + handleEditorChange(null); + } + + @Override + public void partDeactivated(final IWorkbenchPartReference partRef) { + // No action needed + } + + @Override + public void partOpened(final IWorkbenchPartReference partRef) { + if (partRef.getPart(false) instanceof ITextEditor) { + handleEditorChange((ITextEditor) partRef.getPart(false)); + } + } + + @Override + public void partHidden(final IWorkbenchPartReference partRef) { + // No action needed + } + + @Override + public void partVisible(final IWorkbenchPartReference partRef) { + if (partRef.getPart(false) instanceof ITextEditor) { + handleEditorChange((ITextEditor) partRef.getPart(false)); + } + } + + @Override + public void partInputChanged(final IWorkbenchPartReference partRef) { + if (partRef.getPart(false) instanceof ITextEditor) { + handleEditorChange((ITextEditor) partRef.getPart(false)); + } + } + + private void handleEditorChange(final ITextEditor editor) { + // Cancel any pending notification + if (debounceTask != null) { + debounceTask.cancel(false); + } + + // Schedule a new notification after the debounce period + debounceTask = executor.schedule(() -> { + try { + Map params = createActiveEditorParams(editor); + + // Send notification to language server + languageServer.activeEditorChanged(params); + Activator.getLogger().info("Active editor changed notification sent: " + + (editor != null ? editor.getTitle() : "no editor")); + + } catch (Exception e) { + Activator.getLogger().error("Failed to send active editor changed notification", e); + } + }, DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS); + } + + private Map createActiveEditorParams(final ITextEditor editor) { + Map params = new HashMap<>(); + + if (editor != null) { + // Use existing utility to get file URI + Optional fileUri = QEclipseEditorUtils.getOpenFileUri(editor.getEditorInput()); + if (fileUri.isPresent()) { + Map textDocument = new HashMap<>(); + textDocument.put("uri", fileUri.get()); + params.put("textDocument", textDocument); + + // Use existing utility to get cursor state - but only if this editor is the active one + if (editor == QEclipseEditorUtils.getActiveTextEditor()) { + QEclipseEditorUtils.getActiveSelectionRange().ifPresent(range -> { + Map cursorState = new HashMap<>(); + cursorState.put("range", range); + params.put("cursorState", cursorState); + }); + } + } + } else { + // Editor is null (closed), send null values + params.put("textDocument", null); + params.put("cursorState", null); + } + + return params; + } + + /** + * Register the listener with the workbench. + */ + public static ActiveEditorChangeListener register(final AmazonQLspServer languageServer, final ScheduledExecutorService executor) { + ActiveEditorChangeListener listener = new ActiveEditorChangeListener(languageServer, executor); + + // Register with the current active workbench window + if (PlatformUI.getWorkbench().getActiveWorkbenchWindow() != null) { + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getPartService().addPartListener(listener); + } + + // Register with any new windows that open + PlatformUI.getWorkbench().addWindowListener(new org.eclipse.ui.IWindowListener() { + @Override + public void windowOpened(final org.eclipse.ui.IWorkbenchWindow window) { + window.getPartService().addPartListener(listener); + } + + @Override + public void windowClosed(final org.eclipse.ui.IWorkbenchWindow window) { + window.getPartService().removePartListener(listener); + } + + @Override + public void windowActivated(final org.eclipse.ui.IWorkbenchWindow window) { + // No action needed + } + + @Override + public void windowDeactivated(final org.eclipse.ui.IWorkbenchWindow window) { + // No action needed + } + }); + + return listener; + } + + /** + * Unregister the listener. + */ + public void dispose() { + if (debounceTask != null) { + debounceTask.cancel(true); + } + + // Remove from current workbench windows + for (org.eclipse.ui.IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) { + window.getPartService().removePartListener(this); + } + } +} 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 a0033f3d..81f1bd27 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatViewActionHandler.java @@ -69,6 +69,11 @@ public final void handleCommand(final ParsedCommand parsedCommand, final Browser case TAB_BAR_ACTION: case LIST_MCP_SERVERS: case MCP_SERVER_CLICK: + case LIST_RULES: + case RULE_CLICK: + case PINNED_CONTEXT_ADD: + case PINNED_CONTEXT_REMOVE: + case SEND_PINNED_CONTEXT: chatCommunicationManager.sendMessageToChatServer(command, message); break; case CHAT_INFO_LINK_CLICK: 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 8acc582d..3c4dee27 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 @@ -40,6 +40,11 @@ public enum Command { OPEN_SETTINGS("openSettings"), LIST_MCP_SERVERS("aws/chat/listMcpServers"), MCP_SERVER_CLICK("aws/chat/mcpServerClick"), + LIST_RULES("aws/chat/listRules"), + RULE_CLICK("aws/chat/ruleClick"), + PINNED_CONTEXT_ADD("aws/chat/pinnedContextAdd"), + PINNED_CONTEXT_REMOVE("aws/chat/pinnedContextRemove"), + SEND_PINNED_CONTEXT("aws/chat/sendPinnedContext"), // Auth LOGIN_BUILDER_ID("loginBuilderId"),