diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5c5dbeaa6..ac94110ec 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -57,6 +57,26 @@ To send us a pull request, please:
GitHub provides additional documentation on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
+## Updating Plugin Version
+
+To update the plugin version across all files:
+
+1. **Prerequisites**: Ensure you have Git Bash (Windows) or Terminal (Mac/Linux)
+2. **Run the version script**:
+ ```bash
+ ./update-version.sh [new-version]
+ ```
+ Example: `./update-version.sh 2.7.0`
+
+3. **What gets updated**:
+ - Root `pom.xml` version
+ - All child `pom.xml` parent versions
+ - `plugin/META-INF/MANIFEST.MF` Bundle-Version
+ - `feature/feature.xml` version
+ - `updatesite/category.xml` version references
+
+4. **Build with new version**: `mvn clean install` and `mvn clean package`
+
## Debugging/Running Locally
To test your changes locally, you can run the plugin from your workspace by importing it into Eclipse.
diff --git a/feature/feature.xml b/feature/feature.xml
index bfa2216b9..cdb6a69d1 100644
--- a/feature/feature.xml
+++ b/feature/feature.xml
@@ -2,7 +2,7 @@
+ version="2.5.0.qualifier">
Amazon Q Developer helps users build faster across the entire software development lifecycle by providing tailored responses and code recommendations that conform to their team's internal libraries, proprietary algorithmic techniques, and enterprise code style.
@@ -198,6 +198,6 @@ https://github.com/aws/amazon-q-eclipse/blob/main/attribution.xml
id="amazon-q-eclipse"
download-size="11000"
install-size="0"
- version="2.2.1.qualifier"
+ version="2.5.0.qualifier"
unpack="false"/>
diff --git a/feature/pom.xml b/feature/pom.xml
index 49233e6cf..d95396bdb 100644
--- a/feature/pom.xml
+++ b/feature/pom.xml
@@ -6,7 +6,7 @@
software.aws.toolkits.eclipse
amazon-q-eclipse-group
- 2.2.1-SNAPSHOT
+ 2.5.0-SNAPSHOT
../
diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF
index f24644740..33012d0b2 100644
--- a/plugin/META-INF/MANIFEST.MF
+++ b/plugin/META-INF/MANIFEST.MF
@@ -4,7 +4,7 @@ Bundle-Name: Amazon Q for Eclipse
Bundle-Provider: Amazon Web Services
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-SymbolicName: amazon-q-eclipse;singleton:=true
-Bundle-Version: 2.2.1.qualifier
+Bundle-Version: 2.5.0.qualifier
Automatic-Module-Name: amazon.q.eclipse
Bundle-ActivationPolicy: lazy
Bundle-Activator: software.aws.toolkits.eclipse.amazonq.plugin.Activator
@@ -35,6 +35,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0",
Bundle-Classpath: .,
target/dependency/annotations.jar,
target/dependency/apache-client.jar,
+ target/dependency/arns.jar,
target/dependency/auth.jar,
target/dependency/aws-core.jar,
target/dependency/aws-json-protocol.jar,
diff --git a/plugin/plugin.xml b/plugin/plugin.xml
index 371be6311..f87e519b1 100644
--- a/plugin/plugin.xml
+++ b/plugin/plugin.xml
@@ -567,8 +567,29 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugin/pom.xml b/plugin/pom.xml
index 4f62832d0..400d7422f 100644
--- a/plugin/pom.xml
+++ b/plugin/pom.xml
@@ -6,7 +6,7 @@
software.aws.toolkits.eclipse
amazon-q-eclipse-group
- 2.2.1-SNAPSHOT
+ 2.5.0-SNAPSHOT
../
@@ -91,6 +91,11 @@
regions
${aws.java.sdk.version}
+
+ software.amazon.awssdk
+ arns
+ ${aws.java.sdk.version}
+
software.amazon.awssdk
apache-client
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 069acb638..d2cd60d3b 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/chat/ChatCommunicationManager.java
@@ -72,8 +72,8 @@ public final class ChatCommunicationManager implements EventObserver lastProcessedTimeMap = new ConcurrentHashMap<>();
private static final int MINIMUM_PARTIAL_RESPONSE_LENGTH = 50;
- private static final int MIN_DELAY_BETWEEN_PARTIALS = 500;
- private static final int MAX_DELAY_BETWEEN_PARTIALS = 2500;
+ private static final int MIN_DELAY_BETWEEN_PARTIALS = 250;
+ private static final int MAX_DELAY_BETWEEN_PARTIALS = 1500;
private static final int CHAR_COUNT_FOR_MAX_DELAY = 5000;
private final ConcurrentHashMap partialResultLocks = new ConcurrentHashMap<>();
@@ -258,6 +258,42 @@ 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(ChatUIInboundCommandName.ListRules.getValue(),
+ 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(ChatUIInboundCommandName.RuleClick.getValue(),
+ ruleClickResponse);
+ Activator.getEventBroker().post(ChatUIInboundCommand.class, ruleClickCommand);
+ } catch (Exception e) {
+ Activator.getLogger().error("Error processing ruleClick: " + e);
+ }
+ break;
+ case LIST_AVAILABLE_MODELS:
+ try {
+ Object listModelsResponse = amazonQLspServer.listAvailableModels(message.getData()).get();
+ var listModelsCommand = ChatUIInboundCommand.createCommand(ChatUIInboundCommandName.ListAvailableModels.getValue(),
+ listModelsResponse);
+ Activator.getEventBroker().post(ChatUIInboundCommand.class, listModelsCommand);
+ } catch (Exception e) {
+ Activator.getLogger().error("Error processing listAvailableModels: " + e);
+ }
+ break;
+ case PINNED_CONTEXT_ADD:
+ amazonQLspServer.pinnedContextAdd(message.getData());
+ break;
+ case PINNED_CONTEXT_REMOVE:
+ amazonQLspServer.pinnedContextRemove(message.getData());
+ break;
default:
throw new AmazonQPluginException("Unexpected command received from Chat UI: " + command.toString());
}
@@ -602,7 +638,10 @@ public void handlePartialResultProgressNotification(final ProgressParams params)
// 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);
+ // only update timestamp for rate-limited messages (string body without additional messages)
+ if (!hasAdditionalMessages && body instanceof String) {
+ lastProcessedTimeMap.put(tabId, currentTime);
+ }
}
}
}
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 a9fa44eda..dc7d41388 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,11 @@ 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"),
+ ListAvailableModels("aws/chat/listAvailableModels"),
+ SendPinnedContext("aws/chat/sendPinnedContext");
private final String value;
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
index 566121293..41a8552da 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/configuration/profiles/QDeveloperProfileUtil.java
@@ -17,7 +17,6 @@
import org.eclipse.swt.widgets.Display;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awssdk.utils.StringUtils;
@@ -61,51 +60,36 @@ private QDeveloperProfileUtil() { // prevent initialization
.ofNullable(Activator.getPluginStore().get(ViewConstants.Q_DEVELOPER_PROFILE_SELECTION_KEY))
.map(json -> {
try {
- if (isValidSerializedProfile(json)) {
- return deserializeProfile(json);
+ Activator.getLogger().info("Found cached developer profile during init, attempting to validate and deserialize");
+ QDeveloperProfile profile = deserializeProfile(json);
+ if (isValidCachedProfile(profile)) {
+ Activator.getLogger().info("Loaded cached developer profile: " + profile.getName());
+ return profile;
} else {
- Activator.getLogger().error("Cached profile has invalid format");
+ Activator.getLogger().error("Cached profile has invalid data");
}
} catch (final JsonProcessingException e) {
- Activator.getLogger().error("Failed to process cached profile", e);
+ Activator.getLogger().error("Failed to deserialize cached profile", e);
}
return null;
}).orElse(null);
} catch (Exception e) {
- Activator.getLogger().error("Failed to deserialize developer profile", e);
+ Activator.getLogger().error("Failed to load developer profile during init", e);
}
profileSelectionTask = new CompletableFuture<>();
profiles = new ArrayList<>();
}
- private boolean isValidSerializedProfile(final String profile) throws JsonProcessingException {
- JsonNode node = OBJECT_MAPPER.readTree(profile);
- return node.has("arn") && isValidArn(node.get("arn").asText()) && node.has("name")
- && StringUtils.isNotBlank(node.get("name").asText()) && node.has("accountId")
- && isValidAccountId(node.get("accountId").asText()) && node.has("region")
- && node.get("identityDetails").has("region")
- && isValidRegion(node.get("identityDetails").get("region").asText());
- }
private QDeveloperProfile deserializeProfile(final String json) throws JsonProcessingException {
- QDeveloperProfile deserializedProfile = OBJECT_MAPPER.readValue(json, QDeveloperProfile.class);
-
- if (!isValidProfile(deserializedProfile)) {
- throw new JsonProcessingException("Cached profile has invalid data") {
- private static final long serialVersionUID = 1L;
- };
- }
- return deserializedProfile;
+ return OBJECT_MAPPER.readValue(json, QDeveloperProfile.class);
}
private String serializeProfile(final QDeveloperProfile developerProfile) throws JsonProcessingException {
- if (!isValidProfile(developerProfile)) {
- throw new JsonProcessingException("Developer profile has invalid data") {
- private static final long serialVersionUID = 1L;
- };
+ if (developerProfile == null) {
+ throw new IllegalArgumentException("Unable to serialize null profile");
}
-
return OBJECT_MAPPER.writeValueAsString(developerProfile);
}
@@ -118,6 +102,7 @@ public void initialize() {
Activator.getLoginService().logout();
return null;
}).thenAccept(result -> {
+ Activator.getLogger().info("Fetched developer profiles, validating current customization");
CustomizationUtil.validateCurrentCustomization();
});
savedDeveloperProfile = null;
@@ -135,6 +120,7 @@ public synchronized CompletableFuture> queryForDeveloper
private synchronized CompletableFuture> queryForDeveloperProfilesFuture(
final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) {
+ Activator.getLogger().info("Fetching Q developer profiles...");
return Activator.getLspProvider().getAmazonQServer()
.thenCompose(server -> {
GetConfigurationFromServerParams params = new GetConfigurationFromServerParams(
@@ -142,7 +128,11 @@ private synchronized CompletableFuture> queryForDevelope
CompletableFuture> response = server
.getConfigurationFromServer(params);
return response;
- }).thenApply(this::processConfigurations).exceptionally(throwable -> {
+ }).thenApply(configurations -> {
+ var profiles = processConfigurations(configurations);
+ Activator.getLogger().info("Fetched " + profiles.size() + " Q developer profiles");
+ return profiles;
+ }).exceptionally(throwable -> {
Activator.getLogger().error("Error occurred while fetching the list of Q Developer Profile: ",
throwable);
throw new AmazonQPluginException(throwable);
@@ -155,7 +145,7 @@ public synchronized List queryForDeveloperProfiles(final bool
try {
return queryForDeveloperProfilesFuture(tryApplyCachedProfile, false).get();
} catch (InterruptedException e) {
- Activator.getLogger().error("Interrupted when fetching profile: ", e);
+ Activator.getLogger().error("Error occurred when fetching profile: ", e);
}
return new ArrayList<>();
@@ -169,22 +159,24 @@ public synchronized CompletableFuture getProfileSelectionTaskFuture() {
return profileSelectionTask;
}
- private boolean isValidProfile(final QDeveloperProfile profile) {
- return profile != null && StringUtils.isNotBlank(profile.getName()) && isValidAccountId(profile.getAccountId())
- && isValidArn(profile.getArn()) && isValidRegion(profile.getRegion());
+ private boolean isValidFetchedProfile(final QDeveloperProfile profile) {
+ return profile != null;
+ }
+
+ private boolean isValidCachedProfile(final QDeveloperProfile profile) {
+ return profile != null && isValidAccountId(profile.getAccountId()) && isValidArn(profile.getArn()) && isValidRegion(profile.getRegion());
}
private boolean isValidAccountId(final String accountId) {
- return accountId != null && accountId.matches("^\\d{12}$");
+ return StringUtils.isNotBlank(accountId);
}
private boolean isValidArn(final String arn) {
- return arn != null && arn.matches("^arn:aws:codewhisperer:[a-z]{2}-[a-z]+-\\d:\\d{12}:profile/[A-Z0-9]+$");
+ return StringUtils.isNotBlank(arn);
}
private boolean isValidRegion(final String region) {
- return region != null && region
- .matches("^[a-z]{2}-(central|north|south|east|west|northeast|southeast|northwest|southwest)-(\\d)$");
+ return StringUtils.isNotBlank(region);
}
private List handleSelectedProfile(final List profiles,
@@ -224,6 +216,7 @@ private void setProfiles(final List profiles) {
private boolean handleSingleOrNoProfile(final List profiles,
final boolean tryApplyCachedProfile, final boolean applyProfileUnconditionally) {
if (!profiles.isEmpty() && tryApplyCachedProfile) {
+ Activator.getLogger().info("Found single developer profile, auto-selecting");
setDeveloperProfile(profiles.get(0), true, applyProfileUnconditionally);
return true;
}
@@ -240,7 +233,10 @@ private boolean handleMultipleProfiles(final List profiles,
});
if (isProfileSelected && tryApplyCachedProfile) {
+ Activator.getLogger().info("Using cached profile: " + selectedDeveloperProfile.getName());
setDeveloperProfile(selectedDeveloperProfile, true, applyProfileUnconditionally);
+ } else if (!isProfileSelected) {
+ Activator.getLogger().warn("Cached profile not found in available profiles, user selection required");
}
}
return isProfileSelected;
@@ -250,7 +246,8 @@ private List processConfigurations(
final LspServerConfigurations configurations) {
return Optional.ofNullable(configurations).map(
config -> {
- return config.getConfigurations().stream().filter(this::isValidProfile)
+ // we assume backend would return a valid profile and do not any further validations
+ return config.getConfigurations().stream().filter(this::isValidFetchedProfile)
.collect(Collectors.toList());
})
.orElse(Collections.emptyList());
@@ -263,9 +260,13 @@ public List getDeveloperProfiles() {
}
try {
- return queryForDeveloperProfiles(false);
+ List fetchedProfiles = queryForDeveloperProfiles(false);
+ if (fetchedProfiles == null || fetchedProfiles.isEmpty()) {
+ Activator.getLogger().warn("No developer profiles available");
+ }
+ return fetchedProfiles;
} catch (Exception e) {
- Activator.getLogger().error("Interupted while fetching profiles: " + e);
+ Activator.getLogger().error("Failed to fetch profiles: ", e);
}
return null;
@@ -283,6 +284,7 @@ private CompletableFuture setDeveloperProfile(final QDeveloperProfile deve
return CompletableFuture.completedFuture(null);
}
+ Activator.getLogger().info("Setting developer profile: " + developerProfile.getName());
selectedDeveloperProfile = developerProfile;
saveSelectedProfile();
@@ -340,6 +342,8 @@ private void saveSelectedProfile() {
}
} catch (final JsonProcessingException e) {
Activator.getLogger().error("Failed to cache Q developer profile");
+ } catch (Exception e) {
+ Activator.getLogger().error("Unexpected error while caching Q developer profile", e);
}
}
@@ -347,15 +351,17 @@ public boolean isProfileSelectionRequired() {
if (profiles == null || profiles.isEmpty()) {
try {
queryForDeveloperProfiles(false);
+ if (profiles == null || profiles.isEmpty()) {
+ Activator.getLogger().info("No Q developer profiles found");
+ } else if (profiles.size() == 1) {
+ handleSingleOrNoProfile(profiles, true, false);
+ }
} catch (Exception e) {
- Activator.getLogger().error("Interrupted when fetching profile: ", e);
- }
-
- if (profiles.size() == 1) {
- handleSingleOrNoProfile(profiles, true, false);
+ Activator.getLogger().error("Error occurred when fetching profile: ", e);
+ return false;
}
}
- return profiles.size() > 1;
+ return profiles != null && profiles.size() > 1;
}
public QDeveloperProfile getSelectedProfile() {
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 2f24ebebc..df2d7660b 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/inlineChat/InlineChatUIManager.java
@@ -3,6 +3,8 @@
package software.aws.toolkits.eclipse.amazonq.inlineChat;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@@ -27,13 +29,12 @@
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import software.aws.toolkits.eclipse.amazonq.chat.models.CursorState;
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.ToolkitNotification;
public final class InlineChatUIManager {
@@ -52,6 +53,7 @@ public final class InlineChatUIManager {
private final String decidingMessage = "Accept (Enter) | Reject (Esc)";
private boolean isDarkTheme;
private int latestOffset;
+ private Listener paintListenerRef = null;
private InlineChatUIManager() {
// Prevent instantiation
@@ -134,53 +136,41 @@ protected Control createDialogArea(final Composite parent) {
var composite = (Composite) super.createDialogArea(parent);
composite.setLayout(new GridLayout(1, false));
- inputField = new Text(composite, SWT.SEARCH | SWT.BORDER | SWT.SINGLE);
- if (PluginUtils.getPlatform() == PluginPlatform.WINDOWS) {
- Display.getDefault().asyncExec(() -> {
- inputField.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
- inputField.setText(inputPromptMessage);
- });
+ inputField = new Text(composite, SWT.BORDER | SWT.MULTI);
+ Display.getDefault().asyncExec(() -> {
+ inputField.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
+ inputField.setText(inputPromptMessage);
+ });
- inputField.addKeyListener(new KeyAdapter() {
- @Override
- public void keyPressed(final KeyEvent e) {
- // If this is the first character being typed
- boolean backspace = (e.keyCode == SWT.DEL || e.keyCode == SWT.BS);
- if (inputField.getText().equals(inputPromptMessage)) {
- if (!backspace) {
- inputField.setText("");
- inputField.setForeground(isDarkTheme
- ? Display.getDefault().getSystemColor(SWT.COLOR_WHITE)
- : Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
- }
- e.doit = !backspace;
- } else if (backspace && inputField.getText().length() <= 1) {
- inputField.setText(inputPromptMessage);
- inputField.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
+ inputField.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(final KeyEvent e) {
+ // If this is the first character being typed
+ boolean backspace = (e.keyCode == SWT.DEL || e.keyCode == SWT.BS);
+ if (inputField.getText().equals(inputPromptMessage)) {
+ if (!backspace) {
+ inputField.setText("");
+ inputField.setForeground(isDarkTheme
+ ? Display.getDefault().getSystemColor(SWT.COLOR_WHITE)
+ : Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
}
+ e.doit = !backspace;
+ } else if (backspace && inputField.getText().length() <= 1) {
+ inputField.setText(inputPromptMessage);
+ inputField.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
}
- });
- } else {
- inputField.setMessage(inputPromptMessage);
- }
+ }
+ });
GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
gridData.widthHint = 350;
+ gridData.heightHint = 25;
inputField.setLayoutData(gridData);
- // Enforce maximum character count that can be entered into the input
- inputField.addVerifyListener(e -> {
- String currentText = inputField.getText();
- String newText = currentText.substring(0, e.start) + e.text + currentText.substring(e.end);
- if (newText.length() > maxInputLength) {
- e.doit = false; // Prevent the input
- }
- });
-
inputField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
- if (e.character == SWT.CR || e.character == SWT.LF) {
+ if (e.keyCode == SWT.CR || e.keyCode == SWT.LF) {
// Gather inputs and send back to controller
var userInput = inputField.getText();
if (userInputIsValid(userInput)) {
@@ -235,7 +225,7 @@ private void showPrompt(final String promptText) {
latestOffset = task.getSelectionOffset();
}
currentPaintListener = createPaintListenerPrompt(widget, latestOffset, promptText, isDarkTheme);
- widget.addPaintListener(currentPaintListener);
+ addPaintListenerAndCapture(widget, currentPaintListener);
widget.redraw();
} catch (Exception e) {
Activator.getLogger().error("Failed to create paint listener: " + e.getMessage(), e);
@@ -243,6 +233,43 @@ private void showPrompt(final String promptText) {
});
}
+ private void addPaintListenerAndCapture(final StyledText widget, final PaintListener paintListener) {
+ var listenersBefore = new HashSet<>(Arrays.asList(widget.getListeners(SWT.Paint)));
+ widget.addPaintListener(paintListener);
+ var listenersAfter = widget.getListeners(SWT.Paint);
+ for (var listener : listenersAfter) {
+ if (!listenersBefore.contains(listener)) {
+ if (isAdtPaintListener(listener)) {
+ paintListenerRef = listener;
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * ADT Viewers wrap the paint listener into an internal delegate(PaintListenerDelegate).
+ * which needs to be captured to effectively remove it later
+ * @param listener
+ * @return listener ref
+ */
+ private boolean isAdtPaintListener(final Listener listener) {
+ try {
+ // Use reflection to check if this wraps a PaintListenerDelegate
+ if (listener.getClass().getName().contains("TypedListener")) {
+ var eventListenerField = listener.getClass().getDeclaredField("eventListener");
+ eventListenerField.setAccessible(true);
+ Object eventListener = eventListenerField.get(listener);
+ if (eventListener != null && eventListener.getClass().getName().contains("PaintListenerDelegate")) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ // Ignore reflection errors
+ }
+ return false;
+ }
+
public void updatePromptPosition(final SessionState state) {
try {
int offset = ((ITextViewerExtension5) viewer).modelOffset2WidgetOffset(task.getSelectionOffset());
@@ -340,9 +367,17 @@ private void removeCurrentPaintListener() {
return;
}
try {
- if (viewer.getTextWidget() != null && !viewer.getTextWidget().isDisposed() && currentPaintListener != null) {
- viewer.getTextWidget().removePaintListener(currentPaintListener);
- viewer.getTextWidget().redraw();
+ var widget = viewer.getTextWidget();
+ if (widget != null && !widget.isDisposed() && currentPaintListener != null) {
+ // remove adt specific paint listener if present
+ if (paintListenerRef != null) {
+ widget.removeListener(SWT.Paint, paintListenerRef);
+ paintListenerRef = null;
+ }
+ if (currentPaintListener != null) {
+ widget.removePaintListener(currentPaintListener);
+ }
+ widget.redraw();
currentPaintListener = null;
}
} catch (Exception e) {
@@ -364,7 +399,7 @@ private int calculateIndentOffset(final StyledText widget, final int currentOffs
}
private boolean userInputIsValid(final String input) {
- return input != null && input.length() >= 2 && input.length() < maxInputLength;
+ return input != null && input.length() >= 2;
}
/**
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 d50d5bc77..54ae81373 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,6 @@ public interface AmazonQLspClient extends LanguageClient {
@JsonNotification("aws/didCreateDirectory")
void didCreateDirectory(Object params);
+ @JsonNotification("aws/chat/sendPinnedContext")
+ void sendPinnedContext(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 8f44c782c..6d3cc5f43 100644
--- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java
+++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/AmazonQLspClientImpl.java
@@ -7,6 +7,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -17,7 +18,9 @@
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IWorkspace;
@@ -64,6 +67,7 @@
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.ChatUIInboundCommandName;
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;
@@ -82,8 +86,10 @@
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.AbapUtil;
import software.aws.toolkits.eclipse.amazonq.util.Constants;
import software.aws.toolkits.eclipse.amazonq.util.ObjectMapperFactory;
import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector;
@@ -223,15 +229,10 @@ public final CompletableFuture showDocument(final ShowDocume
} else {
Display.getDefault().syncExec(() -> {
try {
- if (!isUriInWorkspace(uri)) {
- Activator.getLogger().error("Attempted to open file outside workspace: " + uri);
- success[0] = false;
- } else {
- IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
- IFileStore fileStore = EFS.getLocalFileSystem().getStore(new URI(uri));
- IDE.openEditorOnFileStore(page, fileStore);
- success[0] = true;
- }
+ 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;
@@ -538,11 +539,19 @@ public final void didCopyFile(final Object params) {
@Override
public final void didWriteFile(final Object params) {
+ var path = extractFilePathFromParams(params);
+ if (AbapUtil.isAbapFile(path)) {
+ AbapUtil.updateAdtServer(path);
+ }
refreshProjects();
}
@Override
public final void didAppendFile(final Object params) {
+ var path = extractFilePathFromParams(params);
+ if (AbapUtil.isAbapFile(path)) {
+ AbapUtil.updateAdtServer(path);
+ }
refreshProjects();
}
@@ -558,6 +567,7 @@ public final void didCreateDirectory(final Object params) {
private void refreshProjects() {
WorkspaceUtils.refreshAllProjects();
+ WorkspaceUtils.refreshAdtViews();
}
private boolean isUriInWorkspace(final String uri) {
@@ -575,4 +585,91 @@ private boolean isUriInWorkspace(final String uri) {
return false;
}
}
+
+ private String extractFilePathFromParams(final Object params) {
+ if (params instanceof Map) {
+ var map = (Map, ?>) params;
+ Object path = map.get("path");
+ return path != null ? path.toString() : null;
+ }
+ return null;
+ }
+
+ @Override
+ public final void sendPinnedContext(final Object params) {
+ Object updatedParams = params;
+ Optional fileUri = getActiveFileUri();
+ if (fileUri.isPresent()) {
+ Map textDocument = Map.of("uri", fileUri.get());
+ if (params instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map paramsMap = new HashMap<>((Map) params);
+ paramsMap.put("textDocument", textDocument);
+ updatedParams = paramsMap;
+ } else {
+ updatedParams = Map.of("params", params, "textDocument", textDocument);
+ }
+ }
+
+ var sendPinnedContextCommand = ChatUIInboundCommand.createCommand(ChatUIInboundCommandName.SendPinnedContext.getValue(), updatedParams);
+ Activator.getEventBroker().post(ChatUIInboundCommand.class, sendPinnedContextCommand);
+ }
+
+ private Optional getActiveFileUri() {
+ AtomicReference> fileUri = new AtomicReference<>();
+ Display.getDefault().syncExec(() -> {
+ try {
+ fileUri.set(getActiveEditorRelativePath());
+ } catch (Exception e) {
+ Activator.getLogger().error("Error getting active file URI", e);
+ fileUri.set(Optional.empty());
+ }
+ });
+ return fileUri.get();
+ }
+
+ private Optional getActiveEditorRelativePath() {
+ var activeEditor = QEclipseEditorUtils.getActiveTextEditor();
+ if (activeEditor == null) {
+ return Optional.empty();
+ }
+ return QEclipseEditorUtils.getOpenFileUri(activeEditor.getEditorInput())
+ .map(this::getRelativePath);
+ }
+
+ private String getRelativePath(final String absoluteUri) {
+ try {
+ if (StringUtils.isBlank(absoluteUri)) {
+ return absoluteUri;
+ }
+
+ var uri = new URI(absoluteUri);
+ var activeFilePath = new File(uri).getCanonicalPath();
+
+ // Get workspace root path
+ var workspace = ResourcesPlugin.getWorkspace();
+ var workspacePath = workspace.getRoot().getLocation();
+ if (workspacePath == null) {
+ return activeFilePath;
+ }
+
+ var workspaceRoot = workspacePath.toFile().getCanonicalPath();
+ if (StringUtils.isBlank(workspaceRoot)) {
+ return activeFilePath;
+ }
+
+ if (StringUtils.startsWithIgnoreCase(activeFilePath, workspaceRoot)) {
+ var workspaceRootPath = Paths.get(workspaceRoot);
+ var activeFilePathObj = Paths.get(activeFilePath);
+ var relativePath = workspaceRootPath.relativize(activeFilePathObj).normalize();
+ return relativePath.toString().replace('\\', '/');
+ }
+
+ // Not in workspace, return absolute path
+ return activeFilePath;
+ } catch (Exception e) {
+ Activator.getLogger().error("Error occurred when attempting to determine relative path for: " + absoluteUri, e);
+ return absoluteUri;
+ }
+ }
}
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 06c0572d7..642b90414 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