Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger customization notifications on ide, stacking multiple notifications vertically and refactor/add support for more methods in PluginStore #48

Merged
merged 13 commits into from
Oct 4, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

package software.aws.toolkits.eclipse.amazonq.configuration;

import java.nio.charset.StandardCharsets;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import com.google.gson.Gson;

import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;

public final class PluginStore {
private static final Preferences PREFERENCES = Preferences.userRoot().node("software.aws.toolkits.eclipse");
private static final Gson GSON = new Gson();
private PluginStore() {
// Prevent instantiation
}
Expand All @@ -31,4 +34,23 @@ public static void remove(final String key) {
PREFERENCES.remove(key);
}

public static <T> void putObject(final String key, final T value) {
String jsonValue = GSON.toJson(value);
byte[] byteValue = jsonValue.getBytes(StandardCharsets.UTF_8);
PREFERENCES.putByteArray(key, byteValue);
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

While this technically can be done serializing entire objects does present additional risks. In general I would like to be convinced that use cases require it, vs. binding relational configuration under a common key namespace, e.g.:

notifications.foo: 123
notifications.bar: 456

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the same reason which I described here https://github.com/aws/amazon-q-eclipse/pull/48/files#r1787324480

Also, currently when LSP server requests for aws.q, we only return customization object but maybe in near future - we might be returning more than just one single row item. Then in those kindof cases, it would be better to just do a single lookup and return it.

try {
PREFERENCES.flush();
} catch (BackingStoreException e) {
PluginLogger.warn(String.format("Error while saving entry to a preference store - key: %s, value: %s", key, value), e);
}
}

public static <T> T getObject(final String key, final Class<T> type) {
byte[] byteValue = PREFERENCES.getByteArray(key, null);
if (byteValue == null) {
return null;
}
String jsonValue = new String(byteValue, StandardCharsets.UTF_8);
return GSON.fromJson(jsonValue, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@

package software.aws.toolkits.eclipse.amazonq.customization;

import java.util.List;
import java.util.Map;
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.providers.LspProvider;
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;

public final class CustomizationUtil {

Expand All @@ -26,4 +34,17 @@ public static void triggerChangeConfigurationNotification(final Map<String, Obje
}
}

public static CompletableFuture<List<Customization>> listCustomizations() {
GetConfigurationFromServerParams params = new GetConfigurationFromServerParams();
params.setSection("aws.q");
return LspProvider.getAmazonQServer()
.thenCompose(server -> server.getConfigurationFromServer(params))
.thenApply(customizations -> customizations.stream()
.filter(customization -> customization != null && StringUtils.isNotBlank(customization.getName()))
.collect(Collectors.toList()))
.exceptionally(throwable -> {
PluginLogger.error("Error occurred while fetching the list of customizations", throwable);
throw new AmazonQPluginException(throwable);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4e.LanguageClientImpl;
Expand All @@ -17,6 +18,7 @@
import software.aws.toolkits.eclipse.amazonq.lsp.model.ConnectionMetadata;
import software.aws.toolkits.eclipse.amazonq.lsp.model.SsoProfileData;
import software.aws.toolkits.eclipse.amazonq.util.Constants;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;
import software.aws.toolkits.eclipse.amazonq.util.PluginLogger;
import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils;
import software.aws.toolkits.eclipse.amazonq.views.AmazonQChatViewActionHandler;
Expand Down Expand Up @@ -47,9 +49,9 @@ public final CompletableFuture<List<Object>> configuration(final ConfigurationPa
List<Object> output = new ArrayList<>();
configurationParams.getItems().forEach(item -> {
if (item.getSection().equals(Constants.LSP_CONFIGURATION_KEY)) {
String customizationArn = PluginStore.get(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY);
Customization storedCustomization = PluginStore.getObject(Constants.CUSTOMIZATION_STORAGE_INTERNAL_KEY, Customization.class);
Map<String, String> customization = new HashMap<>();
customization.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, customizationArn);
customization.put(Constants.LSP_CUSTOMIZATION_CONFIGURATION_KEY, Objects.nonNull(storedCustomization) ? storedCustomization.getArn() : null);
output.add(customization);
} else {
output.add(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package software.aws.toolkits.eclipse.amazonq.lsp;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
Expand All @@ -12,9 +13,11 @@
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.GenericTabParams;
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.UpdateCredentialsPayload;
import software.aws.toolkits.eclipse.amazonq.views.model.Customization;

public interface AmazonQLspServer extends LanguageServer {

Expand All @@ -33,4 +36,6 @@ public interface AmazonQLspServer extends LanguageServer {
@JsonRequest("aws/credentials/token/update")
CompletableFuture<ResponseMessage> updateTokenCredentials(UpdateCredentialsPayload payload);

@JsonRequest("aws/getConfigurationFromServer")
CompletableFuture<List<Customization>> getConfigurationFromServer(GetConfigurationFromServerParams params);
}
Original file line number Diff line number Diff line change
@@ -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.lsp.model;

public class GetConfigurationFromServerParams {
private String section;

public final String getSection() {
return this.section;
}

public final void setSection(final String section) {
this.section = section;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ private Constants() {
public static final String CUSTOMIZATION_STORAGE_INTERNAL_KEY = "aws.q.customization.eclipse";
public static final String LSP_CUSTOMIZATION_CONFIGURATION_KEY = "customization";
public static final String LSP_CONFIGURATION_KEY = "aws.q";
public static final String IDE_CUSTOMIZATION_NOTIFICATION_TITLE = "Amazon Q Customization";
public static final String IDE_CUSTOMIZATION_NOTIFICATION_BODY_TEMPLATE = "Amazon Q inline suggestions are now coming from the %s";
public static final String DEFAULT_Q_FOUNDATION_DISPLAY_NAME = "Amazon Q foundation (Default)";

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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.Monitor;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup;

Expand All @@ -21,6 +24,11 @@ public final class ToolkitNotification extends AbstractNotificationPopup {
private final String title;
private final String description;
private Image infoIcon;
private static CopyOnWriteArrayList<ToolkitNotification> activeNotifications = new CopyOnWriteArrayList<>();
private static final int MAX_WIDTH = 400;
private static final int MIN_HEIGHT = 100;
private static final int PADDING_EDGE = 5;
private static final int NOTIFICATIONS_GAP = 5;

public ToolkitNotification(final Display display, final String title, final String description) {
super(display);
Expand Down Expand Up @@ -56,12 +64,50 @@ protected String getPopupShellTitle() {
}

@Override
protected Point getInitialSize() {
return new Point(60, 20);
protected void initializeBounds() {
Rectangle clArea = getPrimaryClientArea();
Point initialSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT);
int height = Math.max(initialSize.y, MIN_HEIGHT);
int width = Math.min(initialSize.x, MAX_WIDTH);
Point size = new Point(width, height);
// Calculate the position for the new notification
int x = clArea.x + clArea.width - size.x - PADDING_EDGE;
int y = clArea.height + clArea.y - size.y - PADDING_EDGE;
for (ToolkitNotification notification : activeNotifications) {
if (!notification.getShell().isDisposed()) {
y -= notification.getShell().getSize().y + NOTIFICATIONS_GAP;
}
}
getShell().setLocation(x, y);
getShell().setSize(size);
activeNotifications.add(this);
}

private Rectangle getPrimaryClientArea() {
Monitor primaryMonitor = getShell().getDisplay().getPrimaryMonitor();
return primaryMonitor != null ? primaryMonitor.getClientArea() : getShell().getDisplay().getClientArea();
}

private void repositionNotifications() {
Rectangle clArea = getPrimaryClientArea();
Point initialSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT);
int height = Math.max(initialSize.y, MIN_HEIGHT);
int width = Math.min(initialSize.x, MAX_WIDTH);
int x = clArea.x + clArea.width - width - PADDING_EDGE;
int y = clArea.height + clArea.y - height - PADDING_EDGE;
for (ToolkitNotification notification : activeNotifications) {
if (!notification.getShell().isDisposed()) {
Point size = notification.getShell().getSize();
notification.getShell().setLocation(x, y);
y -= size.y + NOTIFICATIONS_GAP;
}
}
}

@Override
public boolean close() {
activeNotifications.remove(this);
repositionNotifications();
if (this.infoIcon != null && !this.infoIcon.isDisposed()) {
this.infoIcon.dispose();
}
Expand Down
Loading