Skip to content

Commit 2ea1fbe

Browse files
committed
feat: implement add-on manager
1 parent c589917 commit 2ea1fbe

17 files changed

+1722
-16
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package de.php_perfect.intellij.ddev.actions;
2+
3+
import com.intellij.openapi.actionSystem.AnActionEvent;
4+
import com.intellij.openapi.project.Project;
5+
import de.php_perfect.intellij.ddev.ui.AddonListPopup;
6+
import org.jetbrains.annotations.NotNull;
7+
8+
/**
9+
* Action to add an add-on to a DDEV project.
10+
*/
11+
public final class DdevAddAddonAction extends DdevAddonAction {
12+
13+
@Override
14+
public void actionPerformed(@NotNull AnActionEvent e) {
15+
Project project = e.getProject();
16+
17+
if (project == null) {
18+
return;
19+
}
20+
21+
new AddonListPopup(project).show();
22+
}
23+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package de.php_perfect.intellij.ddev.actions;
2+
3+
import com.intellij.openapi.actionSystem.ActionUpdateThread;
4+
import com.intellij.openapi.actionSystem.AnActionEvent;
5+
import com.intellij.openapi.project.Project;
6+
import de.php_perfect.intellij.ddev.state.DdevStateManager;
7+
import de.php_perfect.intellij.ddev.state.State;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
/**
11+
* Base class for DDEV add-on related actions.
12+
*/
13+
public abstract class DdevAddonAction extends DdevAwareAction {
14+
15+
@Override
16+
public void update(@NotNull AnActionEvent e) {
17+
super.update(e);
18+
19+
Project project = e.getProject();
20+
21+
if (project == null) {
22+
e.getPresentation().setEnabled(false);
23+
return;
24+
}
25+
26+
e.getPresentation().setEnabled(isActive(project));
27+
}
28+
29+
@Override
30+
protected boolean isActive(@NotNull Project project) {
31+
final State state = DdevStateManager.getInstance(project).getState();
32+
33+
if (!state.isAvailable() || !state.isConfigured()) {
34+
return false;
35+
}
36+
37+
if (state.getDescription() == null) {
38+
return false;
39+
}
40+
41+
return state.getDescription().getStatus() == de.php_perfect.intellij.ddev.cmd.Description.Status.RUNNING;
42+
}
43+
44+
@Override
45+
public @NotNull ActionUpdateThread getActionUpdateThread() {
46+
return ActionUpdateThread.BGT;
47+
}
48+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package de.php_perfect.intellij.ddev.actions;
2+
3+
import com.intellij.openapi.actionSystem.AnActionEvent;
4+
import com.intellij.openapi.project.Project;
5+
import com.intellij.openapi.ui.Messages;
6+
import de.php_perfect.intellij.ddev.cmd.DdevAddon;
7+
import de.php_perfect.intellij.ddev.cmd.DdevRunner;
8+
import de.php_perfect.intellij.ddev.ui.AddonDeletePopup;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import java.util.List;
12+
13+
/**
14+
* Action to remove an add-on from a DDEV project.
15+
*/
16+
public final class DdevDeleteAddonAction extends DdevAddonAction {
17+
18+
@Override
19+
public void actionPerformed(@NotNull AnActionEvent e) {
20+
Project project = e.getProject();
21+
22+
if (project == null) {
23+
return;
24+
}
25+
26+
List<DdevAddon> installedAddons = DdevRunner.getInstance().getInstalledAddons(project);
27+
28+
if (installedAddons.isEmpty()) {
29+
Messages.showInfoMessage(
30+
project,
31+
"No add-ons installed that can be removed.",
32+
"DDEV Add-Ons"
33+
);
34+
return;
35+
}
36+
37+
// Show the custom add-on delete popup
38+
new AddonDeletePopup(project, installedAddons).show();
39+
}
40+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package de.php_perfect.intellij.ddev.addon;
2+
3+
import com.intellij.openapi.application.ApplicationManager;
4+
import com.intellij.openapi.diagnostic.Logger;
5+
import com.intellij.openapi.project.Project;
6+
import com.intellij.openapi.project.ProjectManager;
7+
import com.intellij.openapi.project.ProjectManagerListener;
8+
import de.php_perfect.intellij.ddev.cmd.DdevAddon;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
import java.util.Collections;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.atomic.AtomicBoolean;
17+
import java.util.concurrent.atomic.AtomicLong;
18+
19+
/**
20+
* Cache for DDEV addons to avoid repeated calls to the DDEV CLI.
21+
*/
22+
public class AddonCache {
23+
private static final Logger LOG = Logger.getInstance(AddonCache.class);
24+
private static final ConcurrentHashMap<Project, AddonCache> INSTANCES = new ConcurrentHashMap<>();
25+
// Cache expiration time: 24 hours in milliseconds
26+
private static final long CACHE_EXPIRATION_TIME = 24L * 60 * 60 * 1000;
27+
28+
private final Project project;
29+
private final AtomicBoolean isRefreshing = new AtomicBoolean(false);
30+
private List<DdevAddon> availableAddons = Collections.synchronizedList(Collections.emptyList());
31+
private final AtomicLong lastRefreshTime = new AtomicLong(0);
32+
33+
/**
34+
* Gets the AddonCache instance for the given project.
35+
*
36+
* @param project The project to get the cache for
37+
* @return The AddonCache instance
38+
*/
39+
public static @NotNull AddonCache getInstance(@NotNull Project project) {
40+
return INSTANCES.computeIfAbsent(project, AddonCache::new);
41+
}
42+
43+
private AddonCache(@NotNull Project project) {
44+
this.project = project;
45+
46+
// Initialize the cache in the background
47+
refreshCacheAsync();
48+
49+
// Register a project dispose listener to remove the cache when the project is closed
50+
ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() {
51+
@Override
52+
public void projectClosed(@NotNull Project closedProject) {
53+
if (project.equals(closedProject)) {
54+
INSTANCES.remove(project);
55+
}
56+
}
57+
});
58+
}
59+
60+
/**
61+
* Gets the list of available addons from the cache.
62+
* If the cache is empty or expired, it will be refreshed asynchronously.
63+
*
64+
* @return The list of available addons
65+
*/
66+
public @NotNull List<DdevAddon> getAvailableAddons() {
67+
// If the cache is empty or expired, refresh it asynchronously
68+
if (availableAddons.isEmpty() || isCacheExpired()) {
69+
refreshCacheAsync();
70+
}
71+
return availableAddons;
72+
}
73+
74+
/**
75+
* Checks if the cache is expired.
76+
*
77+
* @return true if the cache is expired, false otherwise
78+
*/
79+
private boolean isCacheExpired() {
80+
return System.currentTimeMillis() - lastRefreshTime.get() > CACHE_EXPIRATION_TIME;
81+
}
82+
83+
/**
84+
* Refreshes the cache asynchronously.
85+
*/
86+
public void refreshCacheAsync() {
87+
// If already refreshing, don't start another refresh
88+
if (isRefreshing.compareAndSet(false, true)) {
89+
LOG.debug("Starting async refresh of addon cache for project: " + project.getName());
90+
91+
ApplicationManager.getApplication().executeOnPooledThread(() -> {
92+
try {
93+
List<DdevAddon> addons = fetchAvailableAddons();
94+
if (addons != null) {
95+
availableAddons = addons;
96+
lastRefreshTime.set(System.currentTimeMillis());
97+
LOG.info("Refreshed addon cache for project: " + project.getName() + ", found " + addons.size() + " addons");
98+
}
99+
} catch (Exception e) {
100+
LOG.error("Failed to refresh addon cache for project: " + project.getName(), e);
101+
} finally {
102+
isRefreshing.set(false);
103+
}
104+
});
105+
}
106+
}
107+
108+
/**
109+
* Fetches the list of available addons from the DDEV CLI.
110+
*
111+
* @return The list of available addons, or null if the operation failed
112+
*/
113+
@Nullable
114+
private List<DdevAddon> fetchAvailableAddons() {
115+
// Execute the DDEV command to get all available add-ons
116+
Map<String, Object> jsonObject = AddonUtils.executeAddonCommand(project, "list", "--all", "--json-output");
117+
118+
// Parse the JSON response into a list of DdevAddon objects
119+
List<DdevAddon> addons = AddonUtils.parseAvailableAddons(jsonObject);
120+
121+
// Return null if no addons were found (to trigger appropriate logging)
122+
return addons.isEmpty() ? null : addons;
123+
}
124+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package de.php_perfect.intellij.ddev.addon;
2+
3+
import com.intellij.openapi.application.ApplicationManager;
4+
import com.intellij.openapi.diagnostic.Logger;
5+
import com.intellij.openapi.project.Project;
6+
import com.intellij.openapi.startup.ProjectActivity;
7+
import de.php_perfect.intellij.ddev.state.DdevStateManager;
8+
import de.php_perfect.intellij.ddev.state.State;
9+
import kotlin.Unit;
10+
import kotlin.coroutines.Continuation;
11+
import org.jetbrains.annotations.NotNull;
12+
import org.jetbrains.annotations.Nullable;
13+
14+
/**
15+
* Startup activity to initialize the AddonCache when the project is opened.
16+
*/
17+
public class AddonCacheStartupActivity implements ProjectActivity {
18+
private static final Logger LOG = Logger.getInstance(AddonCacheStartupActivity.class);
19+
20+
@Nullable
21+
@Override
22+
public Object execute(@NotNull Project project, @NotNull Continuation<? super Unit> continuation) {
23+
LOG.debug("Initializing AddonCache for project: " + project.getName());
24+
25+
// Only initialize the cache if DDEV is available and configured
26+
State state = DdevStateManager.getInstance(project).getState();
27+
if (state.isAvailable() && state.isConfigured()) {
28+
// This will create the cache instance and trigger an async refresh in the background
29+
ApplicationManager.getApplication().executeOnPooledThread(() -> {
30+
AddonCache.getInstance(project);
31+
LOG.info("AddonCache initialized for project: " + project.getName());
32+
});
33+
} else {
34+
LOG.debug("Skipping AddonCache initialization for project: " + project.getName() + " because DDEV is not available or not configured");
35+
}
36+
37+
return Unit.INSTANCE;
38+
}
39+
}

0 commit comments

Comments
 (0)