Skip to content

Commit

Permalink
Feature/plugin actions (#25)
Browse files Browse the repository at this point in the history
* feat: Added action

Signed-off-by: Nahuel Rodriguez <[email protected]>
  • Loading branch information
Nahuel92 authored Jan 14, 2025
1 parent f981739 commit 776f58f
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 21 deletions.
226 changes: 226 additions & 0 deletions src/main/java/io/github/nahuel92/pit4u/action/PIT4UAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package io.github.nahuel92.pit4u.action;

import com.intellij.execution.ExecutionException;
import com.intellij.execution.ProgramRunnerUtil;
import com.intellij.execution.RunManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaDirectoryService;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiPackage;
import com.intellij.psi.PsiQualifiedNamedElement;
import com.intellij.psi.search.FileTypeIndex;
import com.intellij.psi.search.GlobalSearchScopesCore.DirectoryScope;
import io.github.nahuel92.pit4u.configuration.PIT4UConfigurationType;
import io.github.nahuel92.pit4u.configuration.PIT4UEditorStatus;
import io.github.nahuel92.pit4u.configuration.PIT4URunConfiguration;
import io.github.nahuel92.pit4u.icons.PIT4UIcon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.maven.utils.MavenUtil;

import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Collectors;

public class PIT4UAction extends AnAction {
private static final Logger log = Logger.getInstance(PIT4UAction.class);

PIT4UAction() {
getTemplatePresentation().setIcon(PIT4UIcon.ICON);
}

private static boolean shouldShow(final AnActionEvent e) {
final var project = e.getProject();
final var module = e.getData(PlatformCoreDataKeys.MODULE);
if (project == null || module == null) {
return false;
}
final var psiElement = e.getData(CommonDataKeys.PSI_ELEMENT);
if (psiElement instanceof PsiDirectory psiDirectory) {
final var scope = new DirectoryScope(project, psiDirectory.getVirtualFile(), true);
final var containsJavaFiles = FileTypeIndex.containsFileOfType(
JavaFileType.INSTANCE,
scope
);
final var containsTestFiles = FileTypeIndex.processFiles(
JavaFileType.INSTANCE,
a -> a.getPath().contains("test"),
scope
);
return containsJavaFiles && !containsTestFiles;
}
return psiElement instanceof PsiClass psiClass &&
!psiClass.getContainingFile()
.getContainingDirectory()
.getVirtualFile()
.getPath()
.contains("test");
}

private static boolean isProjectRoot(final Project project, final PsiDirectory psiDirectory) {
if (project == null || project.getBasePath() == null) {
return false;
}
return project.getBasePath().equals(psiDirectory.getVirtualFile().getPath());
}

private static void setProjectLabel(final AnActionEvent e) {
final var project = e.getProject();
if (project != null) {
e.getPresentation().setText("Mutate all in " + project.getName());
}
}

private static void setDirectoryLabel(final AnActionEvent e, final PsiDirectory psiDirectory) {
final var javaDirectoryService = JavaDirectoryService.getInstance();
final var dirPackage = javaDirectoryService.getPackage(psiDirectory);
e.getPresentation().setText("Mutate all in " + getDirectoryName(dirPackage, psiDirectory));
}

private static String getDirectoryName(final PsiPackage dirPackage, final PsiDirectory psiDirectory) {
if (dirPackage == null || dirPackage.getQualifiedName().isBlank()) {
return psiDirectory.getName();
}
return dirPackage.getQualifiedName();
}

private static void setClassLabel(final AnActionEvent e, final PsiClass psiClass) {
e.getPresentation().setText("Mutate " + psiClass.getQualifiedName());
}

private static RunnerAndConfigurationSettings getRunConfig(final RunManager runManager) {
final var runConfig = runManager.findConfigurationByName("PIT4URunConfiguration");
if (runConfig != null) {
return runConfig;
}
final var newRunConfig = runManager.createConfiguration(
"PIT4U Action",
PIT4UConfigurationType.class
);
runManager.addConfiguration(newRunConfig);
runManager.setSelectedConfiguration(newRunConfig);
return newRunConfig;
}

private static PIT4UEditorStatus getPit4UEditorStatus(final AnActionEvent e, final Project project, final String basePath) {
final var status = new PIT4UEditorStatus();
setSourceAndReportDirs(e, status, basePath);
final var fullyQualifiedPackages = getFullyQualifiedPackages(e, project);
status.setTargetClasses(fullyQualifiedPackages);
status.setTargetTests(fullyQualifiedPackages);
return status;
}

private static void setSourceAndReportDirs(final AnActionEvent e, final PIT4UEditorStatus status, final String basePath) {
final var module = e.getData(PlatformCoreDataKeys.MODULE);
if (module == null) {
return;
}
final var path = Path.of(basePath);
final var propManager = ExternalSystemModulePropertyManager.getInstance(module);
if (MavenUtil.isMavenModule(module)) {
status.setReportDir(path.resolve("target").toString());
status.setSourceDir(path.resolve("src").resolve("main").resolve("java").toString());
return;
}
if ("gradle".equalsIgnoreCase(propManager.getExternalSystemId())) {
status.setReportDir(path.resolve("build").toString());
status.setSourceDir(path.resolve("src").resolve("main").resolve("java").toString());
}
log.info("Module is not using Maven or Gradle as build system!");
}

private static String getFullyQualifiedPackages(final AnActionEvent event, final Project project) {
if (event.getData(CommonDataKeys.PSI_ELEMENT) instanceof PsiQualifiedNamedElement psiQualifiedNamedElement) {
return psiQualifiedNamedElement.getQualifiedName();
}

final var selectedFile = event.getData(CommonDataKeys.VIRTUAL_FILE);
if (selectedFile == null) {
return null;
}

final var javaFiles = FileTypeIndex.getFiles(
JavaFileType.INSTANCE,
new DirectoryScope(project, selectedFile, true)
);
return javaFiles.stream()
.map(PsiManager.getInstance(project)::findFile)
.filter(e -> e instanceof PsiJavaFile)
.map(e -> ((PsiJavaFile) e).getPackageName() + ".*")
.collect(Collectors.joining(","));
}

private static void executeRunConfiguration(final RunnerAndConfigurationSettings runConfig) {
final var executionBuilder = getExecEnvBuilder(runConfig);
if (executionBuilder.isEmpty()) {
log.error("ExecutionBuilder is empty");
return;
}
ProgramRunnerUtil.executeConfiguration(executionBuilder.get(), true, true);
}

private static Optional<ExecutionEnvironment> getExecEnvBuilder(final RunnerAndConfigurationSettings runConfig) {
try {
return Optional.of(ExecutionEnvironmentBuilder.create(DefaultRunExecutor.getRunExecutorInstance(), runConfig)
.activeTarget()
.build()
);
} catch (final ExecutionException ex) {
log.error("Failed to create execution environment", ex);
return Optional.empty();
}
}

@Override
@NotNull
public ActionUpdateThread getActionUpdateThread() {
return ActionUpdateThread.BGT;
}

@Override
public void update(@NotNull final AnActionEvent e) {
if (!shouldShow(e)) {
e.getPresentation().setEnabledAndVisible(false);
return;
}
e.getPresentation().setEnabledAndVisible(true);
final var element = e.getData(CommonDataKeys.PSI_ELEMENT);
switch (element) {
case PsiDirectory psiDirectory when isProjectRoot(e.getProject(), psiDirectory) -> setProjectLabel(e);
case PsiDirectory psiDirectory -> setDirectoryLabel(e, psiDirectory);
case PsiClass psiClass -> setClassLabel(e, psiClass);
case null, default -> throw new IllegalStateException("Unexpected value: " + element);
}
}

@Override
public void actionPerformed(@NotNull final AnActionEvent e) {
final var project = e.getProject();
if (project == null || project.getBasePath() == null) {
return;
}
final var runManager = RunManager.getInstance(project);
final var runConfig = getRunConfig(runManager);
if (!(runConfig.getConfiguration() instanceof PIT4URunConfiguration config)) {
log.error("PIT4URunConfiguration wasn't found!");
return;
}
config.setPit4UEditorStatus(getPit4UEditorStatus(e, project, project.getBasePath()));
executeRunConfiguration(runConfig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@

import com.intellij.execution.configurations.ConfigurationTypeBase;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.NotNullLazyValue;
import io.github.nahuel92.pit4u.icons.PIT4UIcon;

import javax.swing.*;
import java.util.Objects;

class PIT4UConfigurationType extends ConfigurationTypeBase {
public class PIT4UConfigurationType extends ConfigurationTypeBase {
private static final String ID = "Pit4UConfigurationType";
private static final Icon ICON = IconLoader.findIcon(
PIT4UConfigurationType.class.getResource("/icons/pit4u.svg"),
false
);

protected PIT4UConfigurationType() {
super(
ID,
"PIT4U",
"PIT runner that assess your tests strength",
NotNullLazyValue.createValue(() -> ICON == null ? AllIcons.Empty : ICON)
NotNullLazyValue.createValue(() -> Objects.requireNonNullElse(PIT4UIcon.ICON, AllIcons.Empty))
);
addFactory(new PIT4UConfigurationFactory(this));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
public class PIT4URunConfiguration extends ModuleBasedConfiguration<JavaRunConfigurationModule, PIT4URunConfiguration>
implements Disposable {
private final Logger log = Logger.getInstance(PIT4URunConfiguration.class);
private final PIT4UEditorStatus pit4UEditorStatus = new PIT4UEditorStatus();
private PIT4UEditorStatus pit4UEditorStatus = new PIT4UEditorStatus();

protected PIT4URunConfiguration(final String name, final Project project, final ConfigurationFactory factory) {
super(name, new JavaRunConfigurationModule(project, true), factory);
Expand Down Expand Up @@ -73,25 +73,24 @@ protected JavaParameters createJavaParameters() {
@NotNull
protected OSProcessHandler startProcess() throws ExecutionException {
final var osProcessHandler = super.startProcess();
final var reportIndexPath = Path.of(pit4UEditorStatus.getReportDir())
.resolve("index.html")
.toAbsolutePath();
osProcessHandler.addProcessListener(
new ProcessAdapter() {
@Override
public void processTerminated(@NotNull final ProcessEvent event) {
final var reportIndexPath = Path.of(pit4UEditorStatus.getReportDir())
.toAbsolutePath()
.resolve("index.html");
if (Files.exists(reportIndexPath)) {
final var reportLink = "file:///" + reportIndexPath;
consoleView.printHyperlink(
"Report ready, click to open it in your browser",
new OpenUrlHyperlinkInfo(reportLink)
);
} else {
consoleView.print(
"Report not available. Please check Pitest output above for information on the error",
ConsoleViewContentType.ERROR_OUTPUT
"Report ready, click to open it on your browser",
new OpenUrlHyperlinkInfo("file:///" + reportIndexPath)
);
return;
}
consoleView.print(
"Pitest execution failed. Please check the output above for more information",
ConsoleViewContentType.ERROR_OUTPUT
);
}
}
);
Expand Down Expand Up @@ -138,4 +137,8 @@ public void writeExternal(@NotNull final Element element) throws WriteExternalEx
public void dispose() {
log.info("PIT4URunConfiguration Disposed");
}

public void setPit4UEditorStatus(final PIT4UEditorStatus pit4UEditorStatus) {
this.pit4UEditorStatus = pit4UEditorStatus;
}
}
12 changes: 12 additions & 0 deletions src/main/java/io/github/nahuel92/pit4u/icons/PIT4UIcon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.nahuel92.pit4u.icons;

import com.intellij.openapi.util.IconLoader;

import javax.swing.*;

public interface PIT4UIcon {
Icon ICON = IconLoader.findIcon(
PIT4UIcon.class.getResource("/icons/pit4u.svg"),
false
);
}
9 changes: 8 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<change-notes><![CDATA[
<h4>Version 0.2.0</h4>
<p><em>New release that brings contextual action and small QOL improvements.</em></p>
<p><em>New release that brings a contextual action and small QOL improvements.</em></p>
<h5>Changes</h5>
<ul>
<li>PIT4U contextual action is now available for Maven and Gradle projects (multi-module projects might not work well)</li>
Expand Down Expand Up @@ -94,6 +94,13 @@
<depends>org.jetbrains.idea.maven</depends>
<depends>com.intellij.gradle</depends>

<actions>
<action id="io.github.nahuel92.pit4u.action.PIT4UAction" class="io.github.nahuel92.pit4u.action.PIT4UAction"
text="Pit4UPopUpMenuAction" description="Mutate all classes in this package">
<add-to-group group-id="ProjectViewPopupMenuRunGroup" anchor="last"/>
</action>
</actions>

<extensions defaultExtensionNs="com.intellij">
<configurationType implementation="io.github.nahuel92.pit4u.configuration.PIT4UConfigurationType"/>
</extensions>
Expand Down

0 comments on commit 776f58f

Please sign in to comment.