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

Introduce ViewRouter Component #337

Merged
merged 31 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
13c55ac
improved handling of lsp failure state
Dec 12, 2024
548a9da
add static mock to lsp connection test case
Dec 13, 2024
374e549
Integrate Event Broker in LspStatusManager
taldekar Jan 27, 2025
6a9f8d3
Add ViewRouter POC
taldekar Jan 28, 2025
d1746a4
Fix code formatting bug
taldekar Jan 28, 2025
091b6e9
Add method to retrieve observables
taldekar Jan 29, 2025
fa11959
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
f934e6c
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
a095805
Add listeners for combined state streams and view update request
taldekar Jan 29, 2025
4ad24a1
Add functionality for hot event streams that track latest event (#340)
taldekar Jan 29, 2025
d8b62b4
Remove active view update request listener
taldekar Jan 30, 2025
eca096f
Add ViewRouter tests
taldekar Jan 30, 2025
d1096a6
Remove public constructor
taldekar Jan 30, 2025
24f9c21
Remove public constructor and unused event in tests
taldekar Jan 30, 2025
eb53a8c
Add comments
taldekar Jan 30, 2025
fb1600e
Add documentation for EventBroker
taldekar Jan 30, 2025
da7b9f9
Remove ViewRouter initialization
taldekar Jan 30, 2025
6cea24d
Remove LspInitializingView from ID enum
taldekar Jan 30, 2025
17c0ebb
Add documentation for ViewRouter
taldekar Jan 30, 2025
f4da1d3
Refactor and enhance EventBroker tests
taldekar Jan 30, 2025
65c07f7
Refactor ViewRouter tests for clarity
taldekar Jan 30, 2025
9f01b0b
Remove PluginState class into separate file
taldekar Jan 30, 2025
ebdbcbf
Add documentation to subscription management logic
taldekar Jan 31, 2025
0276e79
Add support for notifying multiple late-subscribers over time of lat…
taldekar Jan 31, 2025
78d7439
Remove CODE_REFERENCE_VIEW
taldekar Jan 31, 2025
77fc653
Rename newActiveViewId to newActiveView
taldekar Jan 31, 2025
581b0eb
Rename ViewId class to AmazonQViewType
taldekar Jan 31, 2025
b395464
Revert "Integrate Event Broker in LspStatusManager"
taldekar Jan 31, 2025
c49a354
Revert "improved handling of lsp failure state"
taldekar Jan 31, 2025
4ed1bf1
Refactor EventBroker and enhance tests (#343)
taldekar Feb 4, 2025
3b95fcd
Merge branch 'feature/viewRefactor' into taldekar/ViewRouter
taldekar Feb 4, 2025
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
2 changes: 1 addition & 1 deletion plugin/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.31.0",
org.apache.commons.logging;bundle-version="1.2.0",
slf4j.api;bundle-version="2.0.13",
org.apache.commons.lang3;bundle-version="3.14.0"
Bundle-Classpath: target/classes/,
Bundle-Classpath: .,
target/dependency/annotations-2.28.26.jar,
target/dependency/apache-client-2.28.26.jar,
target/dependency/auth-2.28.26.jar,
Expand Down
12 changes: 12 additions & 0 deletions plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
icon="icons/AmazonQ.png"
class="software.aws.toolkits.eclipse.amazonq.views.ChatAssetMissingView">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView"
name="Amazon Q"
icon="icons/AmazonQ.png"
class="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview"
name="Amazon Q"
Expand Down Expand Up @@ -142,6 +148,12 @@
relationship="stack"
visible="false">
</view>
<view
id="software.aws.toolkits.eclipse.amazonq.views.LspStartUpFailedView"
relative="software.aws.toolkits.eclipse.amazonq.views.ToolkitLoginWebview"
relationship="stack"
visible="false">
</view>
</perspectiveExtension>
</extension>
<extension
Expand Down
129 changes: 113 additions & 16 deletions plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,132 @@

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

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
import io.reactivex.rxjava3.subjects.Subject;
import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver;

/**
* A thread-safe event broker that implements the publish-subscribe pattern
* using RxJava.
*
* This broker manages event distribution using BehaviorSubjects, which cache
* the most recent event for each event type. It provides type-safe event
* publishing and subscription, with automatic resource management for
* subscriptions. Events are published and consumed on dedicated threads so
* operations are non-blocking.
*/
public final class EventBroker {

private final Subject<Object> eventBus = PublishSubject.create().toSerialized();
/** Maps event types to their corresponding subjects for event distribution */
private final Map<Class<?>, Subject<Object>> subjectsForType;

/** Tracks all subscriptions for proper cleanup */
private final CompositeDisposable disposableSubscriptions;

public EventBroker() {
subjectsForType = new ConcurrentHashMap<>();
disposableSubscriptions = new CompositeDisposable();
}

public <T> void post(final T event) {
/**
* Posts an event of the specified type to all subscribers and caches it for
* late-subscribers.
*
* @param <T> The type of the event
* @param eventType The class object representing the event type
* @param event The event to publish
*/
public <T> void post(final Class<T> eventType, final T event) {
if (event == null) {
return;
}
eventBus.onNext(event);
getOrCreateSubject(eventType).onNext(event);
}

/**
* Gets or creates a Subject for the specified event type. Creates a new
* serialized BehaviorSubject if none exists.
*
* @param <T> The type of events the subject will handle
* @param eventType The class object representing the event type
* @return A Subject that handles events of the specified type
*/
public <T> Subject<Object> getOrCreateSubject(final Class<T> eventType) {
return subjectsForType.computeIfAbsent(eventType, k -> {
Subject<Object> subject = BehaviorSubject.create().toSerialized();
subject.subscribeOn(Schedulers.computation());
/**
* Configure the subject to automatically track all new subscriptions.
* This ensures that all subscriptions created by this subject are properly managed
* and can be disposed of when needed.
*
* The setup:
* 1. Hooks into the subject subscription lifecycle using doOnSubscribe
* 2. Automatically adds each new subscription to the CompositeDisposable
* 3. Subscribes to start the subscription tracking
*/
subject.doOnSubscribe(subscription -> disposableSubscriptions.add(subscription)).subscribe();
return subject;
});
}

/**
* Creates or retrieves an Observable for the specified event type.
*
* @param <T> The type of events to observe
* @param eventType The class object representing the event type
* @return An Observable that emits events of the specified type
*/
private <T> Observable<T> getOrCreateObservable(final Class<T> eventType) {
return getOrCreateSubject(eventType).ofType(eventType);
}

/**
* Subscribes an observer to events of a specific type. The observer will
* receive events on a computation thread by default. The subscription is
* automatically tracked for disposal management.
*
* @param <T> the type of events to observe
* @param eventType the Class object representing the event type
* @param observer the observer that will handle emitted events
* @return a Disposable that can be used to unsubscribe from the events
*/
public <T> Disposable subscribe(final Class<T> eventType, final EventObserver<T> observer) {
Consumer<T> consumer = new Consumer<>() {
@Override
public void accept(final T event) {
observer.onEvent(event);
}
};

return eventBus.ofType(eventType)
.observeOn(Schedulers.computation())
.subscribe(consumer);
}
return getOrCreateObservable(eventType)
.observeOn(Schedulers.computation()) // subscribe on dedicated thread
.subscribe(observer::onEvent);
}

/**
* Returns an Observable for the specified event type. This Observable can be
* used to create custom subscription chains with additional operators.
*
* @param <T> the type of events the Observable will emit
* @param eventType the Class object representing the event type
* @return an Observable that emits events of the specified type
*/
public <T> Observable<T> ofObservable(final Class<T> eventType) {
return getOrCreateObservable(eventType);
}

/**
* Disposes of all subscriptions managed by this broker by clearing the disposable subscriptions collection.
* This method should be called when the broker is no longer needed to prevent memory leaks.
* After disposal, any existing subscriptions will be terminated and new events will not be delivered
* to their observers.
*
* Note: This only disposes of the subscriptions, not the underlying Observables.
* The EventBroker can be reused after disposal by creating new subscriptions.
*/
public void dispose() {
disposableSubscriptions.clear();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import software.aws.toolkits.eclipse.amazonq.chat.models.GenericCommandParams;
import software.aws.toolkits.eclipse.amazonq.chat.models.SendToPromptParams;
import software.aws.toolkits.eclipse.amazonq.chat.models.TriggerType;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.telemetry.ToolkitTelemetryProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata;
Expand All @@ -27,7 +28,7 @@ public abstract class AbstractQChatEditorActionsHandler extends AbstractHandler
@Override
public final boolean isEnabled() {
try {
return Activator.getLoginService().getAuthState().isLoggedIn();
return Activator.getLoginService().getAuthState().isLoggedIn() && !LspStatusManager.getInstance().lspFailed();
} catch (Exception e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;

import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.views.ViewVisibilityManager;

public class QOpenLoginViewHandler extends AbstractHandler {
@Override
public final Object execute(final ExecutionEvent event) {
if (Activator.getLoginService().getAuthState().isLoggedIn()) {
ViewVisibilityManager.showChatView("statusBar");
if (LspStatusManager.getInstance().lspFailed()) {
ViewVisibilityManager.showLspStartUpFailedView("statusBar");
} else {
ViewVisibilityManager.showLoginView("statusBar");
ViewVisibilityManager.showDefaultView("statusBar");
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ private void updateState(final AuthStateType authStatusType, final LoginType log
*/
AuthState newAuthState = getAuthState();
if (previousAuthState == null || newAuthState.authStateType() != previousAuthState.authStateType()) {
Activator.getEventBroker().post(newAuthState);
Activator.getEventBroker().post(AuthState.class, newAuthState);
}
previousAuthState = newAuthState;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,39 @@
import software.amazon.awssdk.utils.StringUtils;
import software.aws.toolkits.eclipse.amazonq.lsp.encryption.DefaultLspEncryptionManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.RecordLspSetupArgs;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;
import software.aws.toolkits.eclipse.amazonq.providers.LspManagerProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.LanguageServerTelemetryProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.metadata.ExceptionMetadata;
import software.aws.toolkits.eclipse.amazonq.util.ProxyUtil;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.preferences.AmazonQPreferencePage;

public class QLspConnectionProvider extends AbstractLspConnectionProvider {

public QLspConnectionProvider() throws IOException {
super();
LanguageServerTelemetryProvider.setAllStartPoint(Instant.now());
LspManager lspManager = LspManagerProvider.getInstance();
var lspInstallResult = lspManager.getLspInstallation();
try {
LanguageServerTelemetryProvider.setAllStartPoint(Instant.now());
LspManager lspManager = LspManagerProvider.getInstance();
var lspInstallResult = lspManager.getLspInstallation();

setWorkingDirectory(lspInstallResult.getServerDirectory());

setWorkingDirectory(lspInstallResult.getServerDirectory());
var serverCommand = Paths.get(lspInstallResult.getServerDirectory(), lspInstallResult.getServerCommand());
List<String> commands = new ArrayList<>();
commands.add(serverCommand.toString());
commands.add(lspInstallResult.getServerCommandArgs());
commands.add("--stdio");
commands.add("--set-credentials-encryption-key");
setCommands(commands);
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
throw(e);
}

var serverCommand = Paths.get(lspInstallResult.getServerDirectory(), lspInstallResult.getServerCommand());
List<String> commands = new ArrayList<>();
commands.add(serverCommand.toString());
commands.add(lspInstallResult.getServerCommandArgs());
commands.add("--stdio");
commands.add("--set-credentials-encryption-key");
setCommands(commands);
}

@Override
Expand Down Expand Up @@ -70,10 +77,12 @@ public final void start() throws IOException {

lspEncryption.initializeEncryptedCommunication(serverStdIn);
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
emitInitFailure(ExceptionMetadata.scrubException(e));
Activator.getLogger().error("Error occured while initializing communication with Amazon Q Lsp Server", e);
}
} catch (Exception e) {
LspStatusManager.getInstance().setToFailed();
emitInitFailure(ExceptionMetadata.scrubException(e));
throw e;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

public enum LspState {
ACTIVE,
FAILED,
PENDING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.views.ViewVisibilityManager;

public final class LspStatusManager {

private static final LspStatusManager INSTANCE;
private LspState lspState;

static {
INSTANCE = new LspStatusManager();
}

private LspStatusManager() {
lspState = LspState.PENDING;
Activator.getEventBroker().post(LspState.class, lspState);
}

public static LspStatusManager getInstance() {
return INSTANCE;
}


public boolean lspFailed() {
return (lspState == LspState.FAILED);
}

public void setToActive() {
lspState = LspState.ACTIVE;
Activator.getEventBroker().post(LspState.class, lspState);
ViewVisibilityManager.showDefaultView("restart");
}

public void setToFailed() {
if (lspState != LspState.FAILED) {
ViewVisibilityManager.showLspStartUpFailedView("update");
lspState = LspState.FAILED;
}

Activator.getEventBroker().post(LspState.class, lspState);
}
public LspState getLspState() {
return lspState;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.eclipse.lsp4j.services.LanguageServer;

import software.aws.toolkits.eclipse.amazonq.lsp.AmazonQLspServer;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.fetcher.RecordLspSetupArgs;
import software.aws.toolkits.eclipse.amazonq.telemetry.LanguageServerTelemetryProvider;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
Expand Down Expand Up @@ -52,6 +53,7 @@ public void setAmazonQServer(final LanguageServer server) {
future.complete(server);
}
emitInitializeMetric();
LspStatusManager.getInstance().setToActive();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStoreKeys;
import software.aws.toolkits.eclipse.amazonq.lsp.AwsServerCapabiltiesProvider;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState;
import software.aws.toolkits.eclipse.amazonq.lsp.manager.LspStatusManager;
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;
Expand Down Expand Up @@ -141,7 +142,7 @@ public final void onEvent(final AuthState authState) {
// chat view
if (browser != null && !browser.isDisposed() && !chatStateManager.hasPreservedState()) {
Optional<String> content = getContent();
if (!content.isPresent()) {
if (!content.isPresent() && !LspStatusManager.getInstance().lspFailed()) {
canDisposeState = true;
ViewVisibilityManager.showChatAssetMissingView("update");
} else {
Expand Down
Loading