From bddf7fe8d055ca2e921202c36f7e082e0d00b0a4 Mon Sep 17 00:00:00 2001 From: azerr Date: Thu, 9 Nov 2023 16:46:25 +0100 Subject: [PATCH] feat: Auto create Quarkus run config while importing project Fixes #1233 Signed-off-by: azerr --- .../quarkus/QuarkusPostStartupActivity.java | 2 + .../run/QuarkusRunConfigurationManager.java | 212 ++++++++++++++++++ .../run/QuarkusRunConfigurationType.java | 2 +- .../quarkus/settings/QuarkusConfigurable.java | 85 +++++++ .../quarkus/settings/QuarkusView.java | 89 ++++++++ .../settings/UserDefinedQuarkusSettings.java | 99 ++++++++ .../resources/META-INF/lsp4ij-quarkus.xml | 9 + src/main/resources/META-INF/plugin.xml | 1 + .../messages/QuarkusBundle.properties | 7 +- 9 files changed, 504 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusConfigurable.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusView.java create mode 100644 src/main/java/com/redhat/devtools/intellij/quarkus/settings/UserDefinedQuarkusSettings.java diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java index e4719bace..1c2d20794 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/QuarkusPostStartupActivity.java @@ -15,11 +15,13 @@ import com.intellij.openapi.startup.StartupActivity; import com.redhat.devtools.intellij.lsp4mp4ij.classpath.ClasspathResourceChangedManager; import com.redhat.devtools.intellij.lsp4mp4ij.psi.core.project.PsiMicroProfileProjectManager; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfigurationManager; import org.jetbrains.annotations.NotNull; public class QuarkusPostStartupActivity implements StartupActivity, DumbAware { @Override public void runActivity(@NotNull Project project) { + QuarkusRunConfigurationManager.getInstance(project); ClasspathResourceChangedManager.getInstance(project); // Force the instantiation of the manager to be sure that classpath listener // are registered before QuarkusLanguageClient classpath listener diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java new file mode 100644 index 000000000..6d889e6be --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * 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.run; + +import com.intellij.ProjectTopics; +import com.intellij.execution.RunManager; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.execution.dashboard.RunDashboardManager; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectListener; +import com.intellij.openapi.externalSystem.service.project.ProjectDataManager; +import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataImportListener; +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil; +import com.intellij.openapi.externalSystem.util.ExternalSystemUtil; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.ModuleListener; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.concurrency.AppExecutorUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.messages.MessageBusConnection; +import com.redhat.devtools.intellij.quarkus.settings.UserDefinedQuarkusSettings; +import com.redhat.microprofile.psi.quarkus.PsiQuarkusUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.maven.MavenDisposable; +import org.jetbrains.idea.maven.project.MavenImportListener; +import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.jetbrains.plugins.gradle.util.GradleConstants; + +import java.util.*; + +/** + * Quarkus run configuration manager used to: + * + * + */ +public class QuarkusRunConfigurationManager implements Disposable { + + public static QuarkusRunConfigurationManager getInstance(Project project) { + return ServiceManager.getService(project, QuarkusRunConfigurationManager.class); + } + + private final Project project; + private final MessageBusConnection projectConnection; + + public QuarkusRunConfigurationManager(Project project) { + this.project = project; + projectConnection = initializeAutoCreateRunConfigurationWhileImporting(project); + } + + public @Nullable RunnerAndConfigurationSettings findExistingConfigurationFor(@NotNull Module module) { + List configurations = RunManager.getInstance(project).getConfigurationSettingsList(QuarkusRunConfigurationType.class); + if (!configurations.isEmpty()) { + for (RunnerAndConfigurationSettings settings : configurations) { + QuarkusRunConfiguration configuration = (QuarkusRunConfiguration) settings.getConfiguration(); + if (module.equals(configuration.getModule())) { + return settings; + } + } + } + return null; + } + + public @NotNull RunnerAndConfigurationSettings createConfiguration(@NotNull Module module, boolean save) { + var runManager = RunManager.getInstance(module.getProject()); + RunnerAndConfigurationSettings quarkusSettings = runManager.createConfiguration(generateConfigurationName(module, runManager), QuarkusRunConfigurationType.class); + ((QuarkusRunConfiguration) quarkusSettings.getConfiguration()).setModule(module); + if (save) { + quarkusSettings.storeInLocalWorkspace(); + // Save the configuration + runManager.addConfiguration(quarkusSettings); + } + return quarkusSettings; + } + + @NotNull + private static String generateConfigurationName(@NotNull Module module, RunManager runManager) { + String configurationName = module.getName(); + RunnerAndConfigurationSettings existingConfiguration = runManager.findConfigurationByTypeAndName(QuarkusRunConfigurationType.ID, configurationName); + int index = 0; + while (existingConfiguration != null) { + configurationName = module.getName() + " (" + index++ + ")"; + existingConfiguration = runManager.findConfigurationByTypeAndName(QuarkusRunConfigurationType.ID, configurationName); + } + return configurationName; + } + + @NotNull + private MessageBusConnection initializeAutoCreateRunConfigurationWhileImporting(Project project) { + MessageBusConnection projectConnection = project.getMessageBus().connect(MavenDisposable.getInstance(project)); + autoCreateWithMaven(project, projectConnection); + autoCreateWithGradle(project, projectConnection); + return projectConnection; + } + + private void autoCreateWithMaven(Project project, MessageBusConnection projectConnection) { + projectConnection.subscribe(MavenImportListener.TOPIC, new MavenImportListener() { + @Override + public void importFinished(@NotNull Collection importedProjects, @NotNull List newModules) { + if (!UserDefinedQuarkusSettings.getInstance(project).isCreateQuarkusRunConfigurationOnProjectImport()) { + return; + } + + VirtualFile projectDir = ProjectUtil.guessProjectDir(project); + assert projectDir != null; + VirtualFile[] children = projectDir.getChildren(); + boolean isGradleProject = ContainerUtil.exists(children, file -> GradleConstants.KNOWN_GRADLE_FILES.contains(file.getName())); + + List modules = new ArrayList<>(); + for (MavenProject mavenProject : importedProjects) { + MavenProjectsManager projectsManager = MavenProjectsManager.getInstance(project); + Module module = projectsManager.findModule(mavenProject); + if (module != null) { + modules.add(module); + } + } + tryToCreateRunConfigurations(modules); + } + }); + } + + private void autoCreateWithGradle(Project project, MessageBusConnection projectConnection) { + projectConnection.subscribe(ProjectDataImportListener.TOPIC, new ProjectDataImportListener() { + + @Override + public void onImportFinished(@Nullable String projectPath) { + if (!UserDefinedQuarkusSettings.getInstance(project).isCreateQuarkusRunConfigurationOnProjectImport()) { + return; + } + List modules = new ArrayList<>(); + Module[] existingModules = ModuleManager.getInstance(project).getModules(); + for (Module module : existingModules) { + // Check if the module is a Gradle project + if (ExternalSystemApiUtil.isExternalSystemAwareModule(GradleConstants.SYSTEM_ID, module)) { + modules.add(module); + } + } + tryToCreateRunConfigurations(modules); + } + }); + } + + private void tryToCreateRunConfigurations(List modules) { + if (modules.isEmpty()) { + return; + } + ReadAction.nonBlocking(() -> doTryToCreateRunConfigurations(modules)) + .inSmartMode(project) + .submit(AppExecutorUtil.getAppExecutorService()); + } + + private void doTryToCreateRunConfigurations(List modules) { + boolean runConfigurationCreated = false; + for (Module module : modules) { + if (tryToCreateRunConfiguration(module)) { + runConfigurationCreated = true; + } + } + if (runConfigurationCreated) { + addQuarkusRunConfigurationTypeInServicesViewIfNeeded(); + } + } + + private boolean tryToCreateRunConfiguration(Module module) { + if (!PsiQuarkusUtils.isQuarkusProject(module)) { + return false; + } + // Find existing Quarkus run configuration + RunnerAndConfigurationSettings quarkusSettings = findExistingConfigurationFor(module); + if (quarkusSettings == null) { + // No Quarkus run configuration for the module, create it and save it in the .idea/workspace.xml file + createConfiguration(module, true); + return true; + } + return false; + } + + private void addQuarkusRunConfigurationTypeInServicesViewIfNeeded() { + RunDashboardManager runDashboardManager = RunDashboardManager.getInstance(project); + Set types = new HashSet<>(runDashboardManager.getTypes()); + if (!types.contains(QuarkusRunConfigurationType.ID)) { + types.add(QuarkusRunConfigurationType.ID); + runDashboardManager.setTypes(types); + } + } + + @Override + public void dispose() { + projectConnection.disconnect(); + } +} 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..d011b42e3 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 @@ -19,7 +19,7 @@ import javax.swing.Icon; public class QuarkusRunConfigurationType implements ConfigurationType { - private static final String ID = "QuarkusRunConfiguration"; + public static final String ID = "QuarkusRunConfiguration"; @NotNull @Override diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusConfigurable.java b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusConfigurable.java new file mode 100644 index 000000000..369e54167 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusConfigurable.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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.settings; + +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.NamedConfigurable; +import com.intellij.openapi.util.NlsContexts; +import com.redhat.devtools.intellij.quarkus.QuarkusBundle; + +import javax.swing.*; + +/** + * Quarkus configuration. + */ +public class QuarkusConfigurable extends NamedConfigurable { + + private final Project project; + private QuarkusView myView; + + public QuarkusConfigurable(Project project) { + this.project = project; + } + + @Override + public UserDefinedQuarkusSettings getEditableObject() { + return null; + } + + @Override + public @NlsContexts.DetailedDescription String getBannerSlogan() { + return null; + } + + @Override + public JComponent createOptionsPanel() { + if (myView == null) { + myView = new QuarkusView(); + } + return myView.getComponent(); + } + + @Override + public void setDisplayName(String name) { + } + + @Override + public @NlsContexts.ConfigurableName String getDisplayName() { + return QuarkusBundle.message("quarkus"); + } + + + @Override + public void reset() { + if (myView == null) return; + UserDefinedQuarkusSettings settings = UserDefinedQuarkusSettings.getInstance(project); + myView.setCreateQuarkusRunConfigurationOnProjectImport(settings.isCreateQuarkusRunConfigurationOnProjectImport()); + } + + @Override + public boolean isModified() { + if (myView == null) return false; + UserDefinedQuarkusSettings settings = UserDefinedQuarkusSettings.getInstance(project); + return !(myView.isCreateQuarkusRunConfigurationOnProjectImport() == settings.isCreateQuarkusRunConfigurationOnProjectImport()); + } + + @Override + public void apply() throws ConfigurationException { + if (myView == null) return; + UserDefinedQuarkusSettings settings = UserDefinedQuarkusSettings.getInstance(project); + settings.setCreateQuarkusRunConfigurationOnProjectImport(myView.isCreateQuarkusRunConfigurationOnProjectImport()); + settings.fireStateChanged(); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusView.java b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusView.java new file mode 100644 index 000000000..9517ec3dc --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/QuarkusView.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * 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.settings; + +import com.intellij.openapi.Disposable; +import com.intellij.ui.IdeBorderFactory; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.util.ui.FormBuilder; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.UI; +import com.redhat.devtools.intellij.quarkus.QuarkusBundle; + +import javax.swing.*; +import javax.swing.border.TitledBorder; + +/** + * Quarkus view. + */ +public class QuarkusView implements Disposable { + + private final JPanel myMainPanel; + + private JBCheckBox createQuarkusRunConfigurationOnProjectImportCheckBox = new JBCheckBox(QuarkusBundle.message("quarkus.create.quarkus.run.configuration.on.project.import")); + + public QuarkusView() { + JComponent descriptionPanel = createDescription(null); + JPanel settingsPanel = createSettings(descriptionPanel); + TitledBorder title = IdeBorderFactory.createTitledBorder(QuarkusBundle.message("quarkus.title")); + settingsPanel.setBorder(title); + this.myMainPanel = JBUI.Panels.simplePanel(10, 10) + .addToLeft(JBUI.Panels.simplePanel()) + .addToCenter(settingsPanel); + } + + private JPanel createSettings(JComponent description) { + return FormBuilder.createFormBuilder() + .addComponent(description, 0) + .addComponent(createQuarkusRunConfigurationOnProjectImportCheckBox, 5) + .addComponentFillVertically(new JPanel(), 0) + .getPanel(); + } + + private JComponent createDescription(String description) { + /** + * Normally comments are below the controls. + * Here we want the comments to precede the controls, we therefore create an empty, 0-sized panel. + */ + JPanel titledComponent = UI.PanelFactory.grid().createPanel(); + titledComponent.setMinimumSize(JBUI.emptySize()); + titledComponent.setPreferredSize(JBUI.emptySize()); + if (description != null && !description.isBlank()) { + titledComponent = UI.PanelFactory.panel(titledComponent) + .withComment(description) + .resizeX(true) + .resizeY(true) + .createPanel(); + } + return titledComponent; + } + + public JComponent getComponent() { + return myMainPanel; + } + + + public boolean isCreateQuarkusRunConfigurationOnProjectImport() { + return createQuarkusRunConfigurationOnProjectImportCheckBox.isSelected(); + } + + public void setCreateQuarkusRunConfigurationOnProjectImport(boolean createQuarkusRunConfigurationOnProjectImport) { + createQuarkusRunConfigurationOnProjectImportCheckBox.setSelected(createQuarkusRunConfigurationOnProjectImport); + } + + @Override + public void dispose() { + + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/settings/UserDefinedQuarkusSettings.java b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/UserDefinedQuarkusSettings.java new file mode 100644 index 000000000..019c6ffc1 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/settings/UserDefinedQuarkusSettings.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * 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.settings; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.project.Project; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.annotations.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * User defined Quarkus settings for: + * + *
    + *
  • auto create Quarkus run configuration
  • + *
+ */ +@State( + name = "QuarkusSettingsState", + storages = {@Storage("quarkusSettings.xml")} +) +public class UserDefinedQuarkusSettings implements PersistentStateComponent { + + private volatile MyState myState = new MyState(); + + private final Project project; + + public UserDefinedQuarkusSettings(Project project) { + this.project = project; + } + + private final List myChangeHandlers = ContainerUtil.createConcurrentList(); + + public static @NotNull UserDefinedQuarkusSettings getInstance(@NotNull Project project) { + return project.getService(UserDefinedQuarkusSettings.class); + } + + public void addChangeHandler(Runnable runnable) { + myChangeHandlers.add(runnable); + } + + public void removeChangeHandler(Runnable runnable) { + myChangeHandlers.remove(runnable); + } + + public void fireStateChanged() { + for (Runnable handler : myChangeHandlers) { + handler.run(); + } + } + public boolean isCreateQuarkusRunConfigurationOnProjectImport() { + return myState.myCreateQuarkusRunConfigurationOnProjectImport; + } + + public void setCreateQuarkusRunConfigurationOnProjectImport(boolean createQuarkusRunConfigurationOnProjectImport) { + myState.myCreateQuarkusRunConfigurationOnProjectImport = createQuarkusRunConfigurationOnProjectImport; + } + + @Nullable + @Override + public MyState getState() { + return myState; + } + + @Override + public void loadState(@NotNull MyState state) { + myState = state; + for (Runnable handler : myChangeHandlers) { + handler.run(); + } + } + + public static class MyState { + + @Tag("createQuarkusRunConfigurationOnProjectImport") + public boolean myCreateQuarkusRunConfigurationOnProjectImport = true; + + MyState() { + } + + } + +} diff --git a/src/main/resources/META-INF/lsp4ij-quarkus.xml b/src/main/resources/META-INF/lsp4ij-quarkus.xml index a2892bf07..0bf0fe817 100644 --- a/src/main/resources/META-INF/lsp4ij-quarkus.xml +++ b/src/main/resources/META-INF/lsp4ij-quarkus.xml @@ -47,6 +47,15 @@ + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d62a9349d..ae4b81bbf 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -338,6 +338,7 @@ + diff --git a/src/main/resources/messages/QuarkusBundle.properties b/src/main/resources/messages/QuarkusBundle.properties index 0aa090119..e6b477648 100644 --- a/src/main/resources/messages/QuarkusBundle.properties +++ b/src/main/resources/messages/QuarkusBundle.properties @@ -11,6 +11,11 @@ # Red Hat Inc. - initial API and implementation ############################################################################### +## Quarkus UI settings page +quarkus=Quarkus +quarkus.title=Quarkus configuration +quarkus.create.quarkus.run.configuration.on.project.import=Create "Quarkus Dev Mode" run configuration on project import + ## Quarkus explorer quarkus.tool.window.display.name=Projects @@ -22,4 +27,4 @@ quarkus.wizard.loading.streams=Loading Quarkus platform streams... quarkus.wizard.error.extensions.loading=Failed to load Quarkus extensions quarkus.wizard.error.extensions.loading.message=Failed to load extensions for Quarkus {0}:\n{1} -quarkus.wizard.loading.extensions=Loading Quarkus extensions... \ No newline at end of file +quarkus.wizard.loading.extensions=Loading Quarkus extensions...