Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> fileUri = QEclipseEditorUtils.getOpenFileUri(activeEditor.getEditorInput());
if (fileUri.isPresent()) {
Map<String, Object> textDocument = new HashMap<>();
textDocument.put("uri", fileUri.get());

if (params instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> paramsMap = new HashMap<>((Map<String, Object>) params);
paramsMap.put("textDocument", textDocument);
updatedParams = paramsMap;
} else {
Map<String, Object> 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why has this been added here if its not implemented yet

// 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,22 @@ <T extends Configuration> CompletableFuture<LspServerConfigurations<T>> getConfi

@JsonRequest("aws/chat/mcpServerClick")
CompletableFuture<Object> mcpServerClick(Object params);

@JsonRequest("aws/chat/listRules")
CompletableFuture<Object> listRules(Object params);

@JsonRequest("aws/chat/ruleClick")
CompletableFuture<Object> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ private Map<String, Object> 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<String, Object> window = new HashMap<>();
window.put("showSaveFileDialog", true);
Expand Down
Original file line number Diff line number Diff line change
@@ -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(() -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why debounce?

try {
Map<String, Object> 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<String, Object> createActiveEditorParams(final ITextEditor editor) {
Map<String, Object> params = new HashMap<>();

if (editor != null) {
// Use existing utility to get file URI
Optional<String> fileUri = QEclipseEditorUtils.getOpenFileUri(editor.getEditorInput());
if (fileUri.isPresent()) {
Map<String, String> 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<String, Object> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down