From a38446306ec5983297d74e4ac133d058d72a256e Mon Sep 17 00:00:00 2001 From: azerr Date: Fri, 22 Sep 2023 17:02:51 +0200 Subject: [PATCH] feat: Create a Quarkus Dev Explorer in the IDE Fixes #1175 Signed-off-by: azerr --- .../core/project/PsiMicroProfileProject.java | 3 + .../intellij/quarkus/QuarkusBundle.java | 34 +-- .../quarkus/explorer/QuarkusActionNode.java | 68 ++++++ .../quarkus/explorer/QuarkusExplorer.java | 210 ++++++++++++++++++ .../explorer/QuarkusOpenDevUINode.java | 18 ++ .../quarkus/explorer/QuarkusProjectNode.java | 43 ++++ .../explorer/QuarkusRunApplicationNode.java | 134 +++++++++++ .../explorer/QuarkusToolWindowFactory.java | 23 ++ .../quarkus/explorer/QuarkusTreeRenderer.java | 134 +++++++++++ .../actions/QuarkusDevStartAction.java | 65 ++++++ .../explorer/actions/QuarkusTreeAction.java | 73 ++++++ .../quarkus/lang/QuarkusIconProvider.java | 3 +- .../intellij/quarkus/lang/QuarkusIcons.java | 26 +++ .../lang/QuarkusServerIconProvider.java | 2 +- .../quarkus/run/QuarkusDevActionGroup.java | 4 +- .../quarkus/run/QuarkusOpenDevUIAction.java | 5 +- .../quarkus/run/QuarkusRunConfiguration.java | 37 ++- .../run/QuarkusRunConfigurationType.java | 3 +- .../quarkus/run/QuarkusRunContext.java | 13 +- src/main/resources/META-INF/plugin.xml | 10 + 20 files changed, 875 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusActionNode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusExplorer.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusOpenDevUINode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusProjectNode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusRunApplicationNode.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusToolWindowFactory.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusTreeRenderer.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusDevStartAction.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusTreeAction.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIcons.java diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProject.java b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProject.java index 76f8c3e9b..834f5ff68 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProject.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4mp4ij/psi/core/project/PsiMicroProfileProject.java @@ -299,4 +299,7 @@ public String getValue(String key) { return provider; } + public Module getJavaProject() { + return javaProject; + } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusBundle.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusBundle.java index 69822d720..b7c7d987b 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusBundle.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusBundle.java @@ -26,20 +26,22 @@ */ public final class QuarkusBundle extends DynamicBundle { - @NonNls public static final String BUNDLE = "messages.QuarkusBundle"; - private static final QuarkusBundle INSTANCE = new QuarkusBundle(); - - private QuarkusBundle() { - super(BUNDLE); - } - - @NotNull - public static @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { - return INSTANCE.getMessage(key, params); - } - - @NotNull - public static Supplier<@Nls String> messagePointer(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { - return INSTANCE.getLazyMessage(key, params); - } + @NonNls + public static final String BUNDLE = "messages.QuarkusBundle"; + private static final QuarkusBundle INSTANCE = new QuarkusBundle(); + + private QuarkusBundle() { + super(BUNDLE); + } + + @NotNull + public static @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { + return INSTANCE.getMessage(key, params); + } + + @NotNull + public static Supplier<@Nls String> messagePointer(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { + return INSTANCE.getLazyMessage(key, params); + } + } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusActionNode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusActionNode.java new file mode 100644 index 000000000..c720338c4 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusActionNode.java @@ -0,0 +1,68 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.ui.AnimatedIcon; +import com.intellij.ui.treeStructure.Tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +public abstract class QuarkusActionNode extends DefaultMutableTreeNode { + + private static final Icon RUNNING_ICON = new AnimatedIcon.Default(); + private final String name; + + private final QuarkusProjectNode projectNode; + + public QuarkusActionNode(String name, QuarkusProjectNode projectNode) { + this.name = name; + this.projectNode = projectNode; + } + + public String getName() { + return name; + } + + public Module getModule() { + return projectNode.getModule(); + } + + public QuarkusProjectNode getProjectNode() { + return projectNode; + } + + public String getDisplayName() { + return name; + } + + public Icon getIcon() { + return AllIcons.Actions.InlayGear; + } + + public void refreshNode(boolean nodeStructureChanged) { + invokeLater(() -> { + var tree = projectNode.getTree(); + if (nodeStructureChanged) { + ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(this); + } else { + ((DefaultTreeModel) tree.getModel()).nodeChanged(this); + } + var treePath = new TreePath(this.getPath()); + tree.expandPath(treePath); + }); + } + + private static void invokeLater(Runnable runnable) { + if (ApplicationManager.getApplication().isDispatchThread()) { + runnable.run(); + } else { + ApplicationManager.getApplication().invokeLater(runnable); + } + } + + public abstract String getActionId(); +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusExplorer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusExplorer.java new file mode 100644 index 000000000..c987cc8d9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusExplorer.java @@ -0,0 +1,210 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.execution.ExecutionListener; +import com.intellij.execution.ExecutionManager; +import com.intellij.execution.RunManagerListener; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.ide.DataManager; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.ui.AnimatedIcon; +import com.intellij.ui.DoubleClickListener; +import com.intellij.ui.treeStructure.Tree; +import com.intellij.util.concurrency.AppExecutorUtil; +import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.quarkus.explorer.actions.QuarkusDevStartAction; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; +import com.redhat.microprofile.psi.quarkus.PsiQuarkusUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.Enumeration; + +import static com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration.QUARKUS_CONFIGURATION; + +public class QuarkusExplorer extends SimpleToolWindowPanel implements Disposable { + + private final Tree tree; + private final Project project; + + public QuarkusExplorer(@NotNull Project project) { + super(true, true); + this.project = project; + tree = buildTree(); + this.setContent(tree); + load(); + } + + /** + * Builds the Language server tree + * + * @return Tree object of all language servers + */ + private Tree buildTree() { + + DefaultMutableTreeNode top = new DefaultMutableTreeNode("Quarkus projects"); + + Tree tree = new Tree(top); + tree.setRootVisible(false); + tree.setCellRenderer(new QuarkusTreeRenderer()); + + tree.putClientProperty(AnimatedIcon.ANIMATION_IN_RENDERER_ALLOWED, true); + + ((DefaultTreeModel) tree.getModel()).reload(top); + + + var doubleClickListener = new DoubleClickListener() { + @Override + protected boolean onDoubleClick(@NotNull MouseEvent event) { + executeAction(tree); + return false; + } + }; + doubleClickListener.installOn(tree); + + tree.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + executeAction(tree); + } + } + }); + + MessageBusConnection connection = project.getMessageBus().connect(project); + connection.subscribe(RunManagerListener.TOPIC, new RunManagerListener() { + @Override + public void runConfigurationSelected(@Nullable RunnerAndConfigurationSettings settings) { + // Do nothing + } + + @Override + public void runConfigurationAdded(@NotNull RunnerAndConfigurationSettings settings) { + // TODO: refresh tree + } + }); + connection.subscribe(ExecutionManager.EXECUTION_TOPIC, new ExecutionListener() { + + @Override + public void processNotStarted(@NotNull String executorId, @NotNull ExecutionEnvironment env) { + QuarkusRunApplicationNode application = findQuarkusApplication(env); + if (application != null) { + application.setApplicationStatus(QuarkusRunApplicationNode.QuarkusApplicationStatus.stopped); + } + } + + @Override + public void processStarted(@NotNull String executorId, @NotNull ExecutionEnvironment env, final @NotNull ProcessHandler handler) { + QuarkusRunApplicationNode application = findQuarkusApplication(env); + if (application != null) { + application.setApplicationStatus(QuarkusRunApplicationNode.QuarkusApplicationStatus.starting); + } + } + + @Override + public void processTerminating(@NotNull String executorId, @NotNull ExecutionEnvironment env, @NotNull ProcessHandler handler) { + QuarkusRunApplicationNode application = findQuarkusApplication(env); + if (application != null) { + application.setApplicationStatus(QuarkusRunApplicationNode.QuarkusApplicationStatus.stopping); + } + } + + @Override + public void processTerminated(@NotNull String executorId, @NotNull ExecutionEnvironment env, @NotNull ProcessHandler handler, int exitCode) { + QuarkusRunApplicationNode application = findQuarkusApplication(env); + if (application != null) { + application.setApplicationStatus(QuarkusRunApplicationNode.QuarkusApplicationStatus.stopped); + } + } + + private @Nullable QuarkusRunApplicationNode findQuarkusApplication(@NotNull ExecutionEnvironment env) { + QuarkusRunConfiguration runConfiguration = env.getDataContext() != null ? (QuarkusRunConfiguration) env.getDataContext().getData(QUARKUS_CONFIGURATION) : null; + if (runConfiguration == null) { + return null; + } + + Module module = runConfiguration.getModule(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode) tree.getModel().getRoot(); + Enumeration children = root.children(); + while (children.hasMoreElements()) { + TreeNode node = children.nextElement(); + if (node instanceof QuarkusProjectNode && module.equals(((QuarkusProjectNode) node).getModule())) { + QuarkusProjectNode project = (QuarkusProjectNode) node; + Enumeration children2 = project.children(); + while (children2.hasMoreElements()) { + TreeNode node2 = children2.nextElement(); + if (node2 instanceof QuarkusRunApplicationNode && runConfiguration.equals(((QuarkusRunApplicationNode) node2).getConfiguration())) { + return (QuarkusRunApplicationNode) node2; + } + } + break; + } + } + return null; + } + }); + return tree; + } + + private void load() { + var action = ReadAction.nonBlocking(() -> { + DefaultMutableTreeNode root = (DefaultMutableTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot(); + Module[] modules = ModuleManager.getInstance(project).getModules(); + for (Module javaProject : modules) { + if (PsiQuarkusUtils.isQuarkusProject(javaProject)) { + QuarkusProjectNode projectNode = new QuarkusProjectNode(javaProject, tree); + root.add(projectNode); + // Fill Quarkus actions + projectNode.add(new QuarkusRunApplicationNode(projectNode)); + } + } + ((DefaultTreeModel) tree.getModel()).reload(root); + }); + var executeInSmartMode = DumbService.getInstance(project).isDumb(); + if (executeInSmartMode) { + action = action.inSmartMode(project); + } + action + .submit(AppExecutorUtil.getAppExecutorService()); + } + + private static void executeAction(Tree tree) { + final TreePath path = tree.getSelectionPath(); + Object node = path.getLastPathComponent(); + if (node instanceof QuarkusActionNode) { + ActionManager am = ActionManager.getInstance(); + String actionId = ((QuarkusActionNode) node).getActionId(); + if (actionId == null) { + return; + } + AnAction action = am.getAction(actionId); + if (action != null) { + action.actionPerformed(new AnActionEvent(null, + DataManager.getInstance().getDataContext(tree), + ActionPlaces.UNKNOWN, new Presentation(), + am, 0)); + } + + } + } + + @Override + public void dispose() { + + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusOpenDevUINode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusOpenDevUINode.java new file mode 100644 index 000000000..31ca8efd7 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusOpenDevUINode.java @@ -0,0 +1,18 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.openapi.module.Module; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.quarkus.run.QuarkusOpenDevUIAction; + +import javax.swing.tree.DefaultTreeModel; + +public class QuarkusOpenDevUINode extends QuarkusActionNode { + public QuarkusOpenDevUINode(QuarkusProjectNode projectNode) { + super("Open Dev UI", projectNode); + } + + @Override + public String getActionId() { + return QuarkusOpenDevUIAction.ACTION_ID; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusProjectNode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusProjectNode.java new file mode 100644 index 000000000..4d8811212 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusProjectNode.java @@ -0,0 +1,43 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.openapi.module.Module; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProject; +import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; +import com.redhat.devtools.intellij.quarkus.lang.QuarkusIcons; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunContext; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; + +public class QuarkusProjectNode extends DefaultMutableTreeNode { + + private final QuarkusRunContext runContext; + + private final Tree tree; + + public QuarkusProjectNode(Module module,Tree tree) { + this.runContext = new QuarkusRunContext(module); + this.tree = tree; + } + + public String getDisplayName() { + return getModule().getName(); + } + + public Icon getIcon() { + return QuarkusIcons.Quarkus; + } + + public Module getModule() { + return runContext.getMicroProfileProject().getJavaProject(); + } + + public QuarkusRunContext getRunContext() { + return runContext; + } + + public Tree getTree() { + return tree; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusRunApplicationNode.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusRunApplicationNode.java new file mode 100644 index 000000000..714edd081 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusRunApplicationNode.java @@ -0,0 +1,134 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.execution.configurations.RunConfiguration; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.AnimatedIcon; +import com.redhat.devtools.intellij.quarkus.explorer.actions.QuarkusDevStartAction; +import com.redhat.devtools.intellij.quarkus.run.QuarkusOpenDevUIAction; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; +import org.eclipse.lsp4j.jsonrpc.CompletableFutures; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeModel; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.CompletableFuture; + +public class QuarkusRunApplicationNode extends QuarkusActionNode { + + private static final Icon RUNNING_ICON = new AnimatedIcon.Default(); + private RunConfiguration configuration; + + private long startTime = -1; + + public void setConfiguration(RunConfiguration configuration) { + this.configuration = configuration; + } + + public RunConfiguration getConfiguration() { + return configuration; + } + + public static enum QuarkusApplicationStatus { + none, + starting, + started, + stopping, + stopped; + } + + private QuarkusApplicationStatus applicationStatus; + + private CompletableFuture startApplicationStatus; + + public QuarkusRunApplicationNode(QuarkusProjectNode projectNode) { + super("Run...", projectNode); + this.applicationStatus = QuarkusApplicationStatus.none; + } + + public void setApplicationStatus(QuarkusApplicationStatus applicationStatus) { + if (startApplicationStatus != null && !startApplicationStatus.isDone()) { + startApplicationStatus.cancel(true); + } + this.applicationStatus = applicationStatus; + switch (applicationStatus) { + case starting: + var projectNode = getProjectNode(); + int port = projectNode.getRunContext().getPort(); + startApplicationStatus = CompletableFutures.computeAsync(cancelChecker -> { + while (!cancelChecker.isCanceled()) { + if (isServerAvailable("localhost", port, 1000)) { + return QuarkusApplicationStatus.started; + } + } + return null; + }).thenAccept(status -> { + if (status == null) { + return; + } + this.applicationStatus = QuarkusApplicationStatus.started; + QuarkusOpenDevUINode node = new QuarkusOpenDevUINode(projectNode); + this.add(node); + refreshNode(true); + }); + startTime = System.currentTimeMillis(); + break; + case started: + startTime = -1; + break; + case stopping: + startTime = System.currentTimeMillis(); + break; + case stopped: + startTime = -1; + this.removeAllChildren(); + refreshNode(true); + break; + default: + refreshNode(false); + } + + } + + @Override + public Icon getIcon() { + switch(applicationStatus) { + case none: + return super.getIcon(); + case started: + return AllIcons.Actions.Commit; + case stopped: + return AllIcons.Actions.Suspend; + default: + return RUNNING_ICON; + } + } + + public String getElapsedTime() { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + return StringUtil.formatDuration(duration, "\u2009"); + } + + public QuarkusApplicationStatus getApplicationStatus() { + return applicationStatus; + } + + private static boolean isServerAvailable(String host, int port, int timeout) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), timeout); + return true; + } catch (IOException e) { + return false; + } + } + + @Override + public String getActionId() { + return QuarkusDevStartAction.ACTION_ID; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusToolWindowFactory.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusToolWindowFactory.java new file mode 100644 index 000000000..fe7cb8315 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusToolWindowFactory.java @@ -0,0 +1,23 @@ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentManager; +import com.redhat.devtools.intellij.lsp4ij.LanguageServerBundle; +import com.redhat.devtools.intellij.quarkus.QuarkusBundle; +import org.jetbrains.annotations.NotNull; + +public class QuarkusToolWindowFactory implements ToolWindowFactory { + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + QuarkusExplorer explorer = new QuarkusExplorer(project); + ContentManager contentManager = toolWindow.getContentManager(); + Content content = contentManager.getFactory().createContent(explorer, + QuarkusBundle.message("quarkus.tool.window.display.name"), false); + content.setDisposer(explorer); + contentManager.addContent(content); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusTreeRenderer.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusTreeRenderer.java new file mode 100644 index 000000000..b20a08dfb --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/QuarkusTreeRenderer.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.explorer; + +import com.intellij.ide.ui.UISettings; +import com.intellij.ui.ColoredTreeCellRenderer; +import com.intellij.ui.RelativeFont; +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.util.ui.UIUtil; +import com.redhat.devtools.intellij.lsp4ij.ServerStatus; +import com.redhat.devtools.intellij.lsp4ij.console.explorer.LanguageServerProcessTreeNode; +import com.redhat.devtools.intellij.lsp4ij.console.explorer.LanguageServerTreeNode; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; + +/** + * Quarkus tree nodes renderer. + *

+ * Some piece of code has been copied/pasted from https://github.com/JetBrains/intellij-community/blob/master/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/ui/TestTreeRenderer.java and adapted for Language Server case. + */ +public class QuarkusTreeRenderer extends ColoredTreeCellRenderer { + + @NonNls + private static final String SPACE_STRING = " "; + private String myDurationText; + private Color myDurationColor; + private int myDurationWidth; + private int myDurationOffset; + + @Override + public void customizeCellRenderer(@NotNull final JTree tree, + final Object value, + final boolean selected, + final boolean expanded, + final boolean leaf, + final int row, + final boolean hasFocus) { + myDurationText = null; + myDurationColor = null; + myDurationWidth = 0; + myDurationOffset = 0; + + if (value instanceof QuarkusProjectNode) { + // Render of Quarkus project + QuarkusProjectNode projectNode = (QuarkusProjectNode) value; + setIcon(projectNode.getIcon()); + append(projectNode.getDisplayName()); + return; + } + + if (value instanceof QuarkusRunApplicationNode) { + // Render of language server process + QuarkusRunApplicationNode applicationNode = (QuarkusRunApplicationNode) value; + setIcon(applicationNode.getIcon()); + append(applicationNode.getDisplayName()); + + if (applicationNode.getApplicationStatus() == QuarkusRunApplicationNode.QuarkusApplicationStatus.starting + || applicationNode.getApplicationStatus() == QuarkusRunApplicationNode.QuarkusApplicationStatus.stopping) { + // Display elapsed time when language server is starting/stopping + myDurationText = applicationNode.getElapsedTime(); + final var durationText = myDurationText; + if (durationText != null) { + FontMetrics metrics = getFontMetrics(RelativeFont.SMALL.derive(getFont())); + myDurationWidth = metrics.stringWidth(durationText); + myDurationOffset = metrics.getHeight() / 2; // an empty area before and after the text + myDurationColor = selected ? UIUtil.getTreeSelectionForeground(hasFocus) : SimpleTextAttributes.GRAYED_ATTRIBUTES.getFgColor(); + } + } + return; + } + + if (value instanceof QuarkusActionNode) { + // Render of Quarkus action + QuarkusActionNode actionNode = (QuarkusActionNode) value; + setIcon(actionNode.getIcon()); + append(actionNode.getDisplayName()); + return; + } + + //strange node + final String text = value.toString(); + //no icon + append(text != null ? text : SPACE_STRING, SimpleTextAttributes.GRAYED_ATTRIBUTES); + } + + @NotNull + @Override + public Dimension getPreferredSize() { + final Dimension preferredSize = super.getPreferredSize(); + if (myDurationWidth > 0) preferredSize.width += myDurationWidth + myDurationOffset; + return preferredSize; + } + + @Override + protected void paintComponent(Graphics g) { + UISettings.setupAntialiasing(g); + Shape clip = null; + int width = getWidth(); + int height = getHeight(); + if (isOpaque()) { + // paint background for expanded row + g.setColor(getBackground()); + g.fillRect(0, 0, width, height); + } + if (myDurationWidth > 0) { + width -= myDurationWidth + myDurationOffset; + if (width > 0 && height > 0) { + g.setColor(myDurationColor); + g.setFont(RelativeFont.SMALL.derive(getFont())); + g.drawString(myDurationText, width + myDurationOffset / 2, getTextBaseLine(g.getFontMetrics(), height)); + clip = g.getClip(); + g.clipRect(0, 0, width, height); + } + } + super.paintComponent(g); + // restore clip area if needed + if (clip != null) g.setClip(clip); + } +} + diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusDevStartAction.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusDevStartAction.java new file mode 100644 index 000000000..b68ff9a09 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusDevStartAction.java @@ -0,0 +1,65 @@ +package com.redhat.devtools.intellij.quarkus.explorer.actions; + +import com.intellij.execution.ExecutorRegistryImpl; +import com.intellij.execution.RunManager; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.execution.configurations.RunConfigurationBase; +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.quarkus.explorer.QuarkusRunApplicationNode; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfigurationType; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.TreePath; +import java.util.List; + +public class QuarkusDevStartAction extends QuarkusTreeAction { + + public static final String ACTION_ID = "com.redhat.devtools.intellij.quarkus.explorer.actions.QuarkusDevStartAction"; + + public QuarkusDevStartAction() { + + } + + @Override + protected void actionPerformed(@NotNull Tree tree, @NotNull AnActionEvent e) { + TreePath path = tree.getSelectionPath(); + QuarkusRunApplicationNode application = path != null ? (QuarkusRunApplicationNode) path.getLastPathComponent() : null; + if (application == null || !(application.getApplicationStatus() == QuarkusRunApplicationNode.QuarkusApplicationStatus.none || application.getApplicationStatus() == QuarkusRunApplicationNode.QuarkusApplicationStatus.stopped)) { + return; + } + Module module = getModule(tree); + if (module == null) { + return; + } + Project project = e.getProject(); + + RunnerAndConfigurationSettings quarkusSettings = null; + List list = RunManager.getInstance(project).getConfigurationSettingsList(QuarkusRunConfigurationType.class); + if (!list.isEmpty()) { + for (RunnerAndConfigurationSettings settings : list) { + QuarkusRunConfiguration configuration = (QuarkusRunConfiguration) settings.getConfiguration(); + if (module.equals(configuration.getModule())) { + quarkusSettings = settings; + break; + } + } + } + if (quarkusSettings == null) { + quarkusSettings = RunManager.getInstance(project).createConfiguration(module.getName(), QuarkusRunConfigurationType.class); + ((QuarkusRunConfiguration) quarkusSettings.getConfiguration()).setModule(module); + } + var dataContext = e.getDataContext(); + var executor = new DefaultRunExecutor(); + + application.setConfiguration(quarkusSettings.getConfiguration()); + ExecutorRegistryImpl.RunnerHelper.run(project, quarkusSettings.getConfiguration(), quarkusSettings, dataContext, executor); + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusTreeAction.java b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusTreeAction.java new file mode 100644 index 000000000..74766cbc9 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/explorer/actions/QuarkusTreeAction.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.explorer.actions; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.module.Module; +import com.intellij.ui.treeStructure.Tree; +import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.lsp4ij.console.explorer.LanguageServerProcessTreeNode; +import com.redhat.devtools.intellij.quarkus.explorer.QuarkusActionNode; +import org.jetbrains.annotations.NotNull; + +import javax.swing.tree.TreePath; +import java.awt.*; + +/** + * Base class for Actions processed from the Language Server tree. + */ +public abstract class QuarkusTreeAction extends AnAction { + + public final void actionPerformed(@NotNull AnActionEvent e) { + Tree tree = getTree(e); + if (tree == null) { + return; + } + actionPerformed(tree, e); + } + + /** + * Returns the language server tree and null otherwise. + * + * @param e the action event. + * @return the language server tree and null otherwise. + */ + private Tree getTree(AnActionEvent e) { + Component component = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); + if (component instanceof Tree) { + return (Tree) component; + } + return null; + } + + /** + * Returns the selected language server tree node and null otherwise. + * + * @param tree the tree. + * @return the selected language server tree node and null otherwise. + */ + protected Module getModule(Tree tree) { + TreePath path = tree.getSelectionPath(); + Object node = path != null ? path.getLastPathComponent() : null; + if (node instanceof QuarkusActionNode) { + return ((QuarkusActionNode) node).getModule(); + } + return null; + } + + protected abstract void actionPerformed(@NotNull Tree tree, @NotNull AnActionEvent e); + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java index 1d089b9a7..426790577 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIconProvider.java @@ -24,7 +24,6 @@ * Quarkus icon provider. */ public class QuarkusIconProvider extends IconProvider { - public static final Icon QUARKUS_ICON = IconLoader.findIcon("/quarkus_icon_rgb_16px_default.png", QuarkusIconProvider.class); @Nullable @Override @@ -34,7 +33,7 @@ public Icon getIcon(@NotNull PsiElement element, int flags) { VirtualFile file = element.getContainingFile().getVirtualFile(); if (QuarkusModuleUtil.isQuarkusPropertiesFile(file, element.getProject()) || QuarkusModuleUtil.isQuarkusYAMLFile(file, element.getProject())) { - return QUARKUS_ICON; + return QuarkusIcons.Quarkus; } } return null; diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIcons.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIcons.java new file mode 100644 index 000000000..4f70d0d63 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusIcons.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2023 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.lang; + +import com.intellij.openapi.util.IconLoader; + +import javax.swing.*; + +/** + * Quarkus icons. + */ +public class QuarkusIcons { + + public static final Icon Quarkus = IconLoader.findIcon("/quarkus_icon_rgb_16px_default.png", QuarkusIcons.class); +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java index 4f7bc9ae6..dc5169e51 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/lang/QuarkusServerIconProvider.java @@ -24,6 +24,6 @@ public class QuarkusServerIconProvider implements ServerIconProvider { @Override public Icon getIcon() { - return QuarkusIconProvider.QUARKUS_ICON; + return QuarkusIcons.Quarkus; } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusDevActionGroup.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusDevActionGroup.java index 88bfaca8c..db426781b 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusDevActionGroup.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusDevActionGroup.java @@ -14,6 +14,7 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.util.IconLoader; +import com.redhat.devtools.intellij.quarkus.lang.QuarkusIcons; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -22,8 +23,7 @@ public class QuarkusDevActionGroup extends ActionGroup { private static final AnAction OPEN_APP_ACTION = new QuarkusOpenAppInBrowserAction(); public QuarkusDevActionGroup() { - super("Quarkus", "", IconLoader.getIcon("/quarkus_icon_rgb_16px_default.png", - QuarkusDevActionGroup.class)); + super("Quarkus", "", QuarkusIcons.Quarkus); setPopup(true); } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusOpenDevUIAction.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusOpenDevUIAction.java index 209501c22..76eacf8cd 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusOpenDevUIAction.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusOpenDevUIAction.java @@ -15,11 +15,14 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.util.IconLoader; import com.redhat.devtools.intellij.quarkus.TelemetryService; +import com.redhat.devtools.intellij.quarkus.lang.QuarkusIcons; import org.jetbrains.annotations.NotNull; public class QuarkusOpenDevUIAction extends QuarkusDevAction { + public static final String ACTION_ID = "com.redhat.devtools.intellij.quarkus.run.QuarkusOpenDevUIAction"; + public QuarkusOpenDevUIAction() { - super("Open DevUI","Launches the DevUI in a browser", IconLoader.getIcon("/quarkus_icon_rgb_16px_default.png", QuarkusOpenDevUIAction.class)); + super("Open DevUI","Launches the DevUI in a browser", QuarkusIcons.Quarkus); } @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java index 17e39a556..cbbb5aec0 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java @@ -30,7 +30,12 @@ import com.intellij.execution.runners.ExecutionEnvironmentBuilder; import com.intellij.execution.runners.ExecutionUtil; import com.intellij.execution.runners.ProgramRunner; +import com.intellij.ide.ui.IdeUiService; +import com.intellij.openapi.actionSystem.CustomizedDataContext; import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.openapi.actionSystem.impl.AsyncDataContext; +import com.intellij.openapi.actionSystem.impl.EdtDataContext; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.options.SettingsEditor; @@ -44,11 +49,13 @@ import com.redhat.devtools.intellij.quarkus.TelemetryService; import com.redhat.devtools.intellij.quarkus.tool.ToolDelegate; import com.redhat.devtools.intellij.telemetry.core.service.TelemetryMessageBuilder; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.*; import java.io.IOException; import java.net.ConnectException; import java.net.ServerSocket; @@ -61,7 +68,7 @@ public class QuarkusRunConfiguration extends ModuleBasedConfiguration { private final static Logger LOGGER = LoggerFactory.getLogger(QuarkusRunConfiguration.class); - private static final String QUARKUS_CONFIGURATION = "Quarkus Configuration"; + public static final String QUARKUS_CONFIGURATION = "Quarkus Configuration"; private int port = 5005; @@ -138,7 +145,7 @@ public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEn RunnerAndConfigurationSettings settings = toolDelegate.getConfigurationDelegate(getModule(), this); if (settings != null) { long groupId = ExecutionEnvironment.getNextUnusedExecutionId(); - doRunConfiguration(settings, executor, DefaultExecutionTarget.INSTANCE, groupId, null, + doRunConfiguration(settings, executor, DefaultExecutionTarget.INSTANCE, groupId, newDataContext(environment), desc -> desc.getComponent().putClientProperty(QuarkusConstants.QUARKUS_RUN_CONTEXT_KEY, new QuarkusRunContext(getModule()))); } } else { @@ -156,6 +163,21 @@ public void run(@NotNull ProgressIndicator indicator) { return null; } + private class MyDataContext implements DataProvider, AsyncDataContext { + + @Override + public @Nullable Object getData(@NotNull @NonNls String dataId) { + if (QUARKUS_CONFIGURATION.equals(dataId)) { + return QuarkusRunConfiguration.this; + } + return null; + } + } + + private DataContext newDataContext(ExecutionEnvironment environment) { + return new MyDataContext(); + } + private void waitForPortAvailable(int port, ProgressIndicator monitor) throws IOException { long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < 60_000 && !monitor.isCanceled()) { @@ -213,10 +235,10 @@ public int getPort() { } private static void doRunConfiguration(@NotNull RunnerAndConfigurationSettings configuration, - @NotNull Executor executor, - @Nullable ExecutionTarget targetOrNullForDefault, - @Nullable Long executionId, - @Nullable DataContext dataContext, + @NotNull Executor executor, + @Nullable ExecutionTarget targetOrNullForDefault, + @Nullable Long executionId, + @Nullable DataContext dataContext, ProgramRunner.Callback callback) { ExecutionEnvironmentBuilder builder = createEnvironment(executor, configuration); if (builder == null) { @@ -225,8 +247,7 @@ private static void doRunConfiguration(@NotNull RunnerAndConfigurationSettings c if (targetOrNullForDefault != null) { builder.target(targetOrNullForDefault); - } - else { + } else { builder.activeTarget(); } if (executionId != null) { diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationType.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationType.java index 843f5a4f0..d16907f88 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationType.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationType.java @@ -13,6 +13,7 @@ import com.intellij.execution.configurations.ConfigurationFactory; import com.intellij.execution.configurations.ConfigurationType; import com.intellij.openapi.util.IconLoader; +import com.redhat.devtools.intellij.quarkus.lang.QuarkusIcons; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -35,7 +36,7 @@ public String getConfigurationTypeDescription() { @Override public Icon getIcon() { - return IconLoader.findIcon("/quarkus_icon_rgb_16px_default.png", QuarkusRunConfigurationType.class); + return QuarkusIcons.Quarkus; } @NotNull diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunContext.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunContext.java index af87f28cf..1386645d4 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunContext.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunContext.java @@ -30,7 +30,12 @@ public QuarkusRunContext(Module module) { } protected static QuarkusRunContext getContext(AnActionEvent e) { - Project project = PlatformDataKeys.PROJECT.getData(e.getDataContext()); + var dataContext = e.getDataContext(); + QuarkusRunContext runContext = (QuarkusRunContext) dataContext.getData(QuarkusConstants.QUARKUS_RUN_CONTEXT_KEY); + if(runContext != null) { + return runContext; + } + Project project = PlatformDataKeys.PROJECT.getData(dataContext); RunContentManager contentManager = RunContentManager.getInstance(project); RunContentDescriptor selectedContent = contentManager.getSelectedContent(); JComponent component = selectedContent == null ? null : selectedContent.getComponent(); @@ -53,7 +58,7 @@ private String normalize(String path) { return builder.toString(); } - private int getPort() { + public int getPort() { int port = project.getPropertyAsInteger("quarkus.http.port", 8080); port = project.getPropertyAsInteger("%dev.quarkus.http.port", port); return port; @@ -76,4 +81,8 @@ public String getApplicationURL() { path = normalize(path); return "http://localhost:" + port + path; } + + public PsiMicroProfileProject getMicroProfileProject() { + return project; + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ea4f68ef0..a788ad10d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -335,6 +335,10 @@ + @@ -582,5 +586,11 @@ + +