Skip to content

Commit 26af6ed

Browse files
committed
#1541 provide linemarker to run Symfony command via internal console terminal
1 parent 13ac25b commit 26af6ed

File tree

6 files changed

+336
-0
lines changed

6 files changed

+336
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic.command;
2+
3+
import com.intellij.execution.Location;
4+
import com.intellij.execution.PsiLocation;
5+
import com.intellij.execution.actions.ConfigurationContext;
6+
import com.intellij.execution.actions.LazyRunConfigurationProducer;
7+
import com.intellij.execution.configurations.ConfigurationFactory;
8+
import com.intellij.execution.configurations.ConfigurationType;
9+
import com.intellij.execution.configurations.RunConfiguration;
10+
import com.intellij.openapi.project.DumbAware;
11+
import com.intellij.openapi.project.Project;
12+
import com.intellij.openapi.util.Ref;
13+
import com.intellij.psi.PsiElement;
14+
import com.jetbrains.php.lang.psi.elements.PhpClass;
15+
import com.jetbrains.php.run.PhpRunConfigurationFactoryBase;
16+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
17+
import org.jetbrains.annotations.Nls;
18+
import org.jetbrains.annotations.NotNull;
19+
20+
import javax.swing.*;
21+
22+
/**
23+
* @author Daniel Espendiller <daniel@espendiller.net>
24+
*/
25+
public class SymfonyCommandLazyRunConfigurationProducer extends LazyRunConfigurationProducer<SymfonyCommandRunConfiguration> {
26+
27+
@NotNull
28+
@Override
29+
public ConfigurationFactory getConfigurationFactory() {
30+
return new PhpScrip().getConfigurationFactories()[0];
31+
}
32+
33+
@Override
34+
protected boolean setupConfigurationFromContext(@NotNull SymfonyCommandRunConfiguration configuration, @NotNull ConfigurationContext context, @NotNull Ref<PsiElement> sourceElement) {
35+
Location location = context.getLocation();
36+
if (location instanceof PsiLocation) {
37+
PhpClass phpClass = SymfonyCommandTestRunLineMarkerProvider.getCommandContext(location.getPsiElement());
38+
if (phpClass != null) {
39+
String commandNameFromClass = SymfonyCommandTestRunLineMarkerProvider.getCommandNameFromClass(phpClass);
40+
if (commandNameFromClass != null) {
41+
configuration.setCommandName(commandNameFromClass);
42+
configuration.setName(commandNameFromClass);
43+
return true;
44+
}
45+
}
46+
}
47+
48+
return false;
49+
}
50+
51+
@Override
52+
public boolean isConfigurationFromContext(@NotNull SymfonyCommandRunConfiguration configuration, @NotNull ConfigurationContext context) {
53+
Location location = context.getLocation();
54+
if (location instanceof PsiLocation) {
55+
PhpClass phpClass = SymfonyCommandTestRunLineMarkerProvider.getCommandContext(location.getPsiElement());
56+
if (phpClass != null) {
57+
return SymfonyCommandTestRunLineMarkerProvider.getCommandNameFromClass(phpClass) != null;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
private static final class PhpScrip implements ConfigurationType, DumbAware {
65+
private final ConfigurationFactory myFactory = new PhpRunConfigurationFactoryBase(this, "Symfony Command") {
66+
@NotNull
67+
public RunConfiguration createTemplateConfiguration(@NotNull Project project) {
68+
return new SymfonyCommandRunConfiguration(project, this, "Symfony Command");
69+
}
70+
71+
@NotNull
72+
public String getName() {
73+
return "Symfony Command";
74+
}
75+
};
76+
77+
public PhpScrip() {
78+
}
79+
80+
@Override
81+
public @NotNull @Nls(capitalization = Nls.Capitalization.Title) String getDisplayName() {
82+
return "Symfony Command";
83+
}
84+
85+
@Override
86+
public @Nls(capitalization = Nls.Capitalization.Sentence) String getConfigurationTypeDescription() {
87+
return "Symfony Command";
88+
}
89+
90+
public Icon getIcon() {
91+
return Symfony2Icons.SYMFONY;
92+
}
93+
94+
public ConfigurationFactory[] getConfigurationFactories() {
95+
return new ConfigurationFactory[]{this.myFactory};
96+
}
97+
98+
@NotNull
99+
public String getId() {
100+
return "symfony.command";
101+
}
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic.command;
2+
3+
import com.intellij.execution.DefaultExecutionResult;
4+
import com.intellij.execution.ExecutionException;
5+
import com.intellij.execution.Executor;
6+
import com.intellij.execution.configurations.ConfigurationFactory;
7+
import com.intellij.execution.configurations.LocatableConfigurationBase;
8+
import com.intellij.execution.configurations.RunConfiguration;
9+
import com.intellij.execution.configurations.RunProfileState;
10+
import com.intellij.execution.impl.ConsoleViewImpl;
11+
import com.intellij.execution.process.KillableProcessHandler;
12+
import com.intellij.execution.process.ProcessHandler;
13+
import com.intellij.execution.process.ScriptRunnerUtil;
14+
import com.intellij.execution.runners.ExecutionEnvironment;
15+
import com.intellij.openapi.options.SettingsEditor;
16+
import com.intellij.openapi.options.SettingsEditorGroup;
17+
import com.intellij.openapi.project.Project;
18+
import com.intellij.openapi.vfs.VirtualFile;
19+
import fr.adrienbrault.idea.symfony2plugin.util.ProjectUtil;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
/**
24+
* @author Daniel Espendiller <daniel@espendiller.net>
25+
*/
26+
public class SymfonyCommandRunConfiguration extends LocatableConfigurationBase<SymfonyCommandRunConfiguration.Config> {
27+
public static class Config {}
28+
29+
@Nullable
30+
private String commandName;
31+
32+
protected SymfonyCommandRunConfiguration(Project project, ConfigurationFactory factory, String name) {
33+
super(project, factory, name);
34+
}
35+
36+
@Override
37+
public @NotNull SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
38+
return new SettingsEditorGroup<>();
39+
}
40+
41+
@Override
42+
public @Nullable RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment environment) throws ExecutionException {
43+
return (executor1, runner) -> {
44+
VirtualFile projectDir = ProjectUtil.getProjectDir(getProject());
45+
46+
ProcessHandler processHandler = ScriptRunnerUtil.execute("bin/console", projectDir.getPath(), null, new String[] {this.commandName}, null, (commandLine) -> {
47+
KillableProcessHandler handler = new KillableProcessHandler(commandLine);
48+
handler.setShouldKillProcessSoftly(false);
49+
return handler;
50+
});
51+
52+
ConsoleViewImpl console = new ConsoleViewImpl(getProject(), true);
53+
console.attachToProcess(processHandler);
54+
return new DefaultExecutionResult(console, processHandler);
55+
};
56+
}
57+
58+
public void setCommandName(@NotNull String commandName) {
59+
this.commandName = commandName;
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic.command;
2+
3+
import com.intellij.execution.actions.BaseRunConfigurationAction;
4+
import com.intellij.execution.actions.RunContextAction;
5+
import com.intellij.execution.executors.DefaultRunExecutor;
6+
import com.intellij.execution.lineMarker.RunLineMarkerContributor;
7+
import com.intellij.icons.AllIcons;
8+
import com.intellij.openapi.actionSystem.AnAction;
9+
import com.intellij.psi.PsiElement;
10+
import com.intellij.util.ObjectUtils;
11+
import com.jetbrains.php.lang.lexer.PhpTokenTypes;
12+
import com.jetbrains.php.lang.psi.PhpPsiUtil;
13+
import com.jetbrains.php.lang.psi.elements.Field;
14+
import com.jetbrains.php.lang.psi.elements.PhpAttribute;
15+
import com.jetbrains.php.lang.psi.elements.PhpClass;
16+
import com.jetbrains.php.lang.psi.elements.PhpNamedElement;
17+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionArgument;
18+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
19+
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
20+
import org.apache.commons.lang.StringUtils;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
24+
import java.util.Collection;
25+
26+
/**
27+
* @author Daniel Espendiller <daniel@espendiller.net>
28+
*/
29+
public class SymfonyCommandTestRunLineMarkerProvider extends RunLineMarkerContributor {
30+
@Override
31+
public @Nullable Info getInfo(@NotNull PsiElement leaf) {
32+
PhpClass phpClass = getCommandContext(leaf);
33+
if (phpClass != null) {
34+
String commandNameFromClass = getCommandNameFromClass(phpClass);
35+
if (commandNameFromClass != null) {
36+
BaseRunConfigurationAction baseRunConfigurationAction = new RunContextAction(DefaultRunExecutor.getRunExecutorInstance());
37+
return new Info(AllIcons.RunConfigurations.TestState.Run, new AnAction[]{baseRunConfigurationAction}, (psiElement) -> "Run Script");
38+
}
39+
}
40+
41+
return null;
42+
}
43+
44+
@Nullable
45+
public static PhpClass getCommandContext(@NotNull PsiElement leaf) {
46+
if (PhpPsiUtil.isOfType(leaf, PhpTokenTypes.IDENTIFIER)) {
47+
PhpNamedElement element = ObjectUtils.tryCast(leaf.getParent(), PhpNamedElement.class);
48+
if (element != null && element.getNameIdentifier() == leaf) {
49+
if (element instanceof PhpClass) {
50+
return (PhpClass) element;
51+
}
52+
}
53+
}
54+
55+
return null;
56+
}
57+
58+
@Nullable
59+
public static String getCommandNameFromClass(@NotNull PhpClass phpClass) {
60+
if (PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Console\\Command\\Command")) {
61+
// lazy naming:
62+
// protected static $defaultName = 'app:create-user'
63+
Field defaultName = phpClass.findFieldByName("defaultName", false);
64+
if (defaultName != null) {
65+
PsiElement defaultValue = defaultName.getDefaultValue();
66+
if (defaultValue != null) {
67+
return PhpElementsUtil.getStringValue(defaultValue);
68+
}
69+
}
70+
71+
// php attributes:
72+
// #[AsCommand(name: 'app:create-user')]
73+
for (PhpAttribute attribute : phpClass.getAttributes("\\Symfony\\Component\\Console\\Attribute\\AsCommand")) {
74+
for (PhpAttribute.PhpAttributeArgument argument : attribute.getArguments()) {
75+
String name = argument.getName();
76+
if ("name".equals(name)) {
77+
PhpExpectedFunctionArgument argument1 = argument.getArgument();
78+
if (argument1 != null) {
79+
String value1 = PsiElementUtils.trimQuote(argument1.getValue());
80+
if (StringUtils.isNotBlank(value1)) {
81+
return value1;
82+
}
83+
}
84+
break;
85+
}
86+
}
87+
}
88+
89+
// @TODO: provide tag resolving here
90+
// - { name: 'console.command', command: 'app:sunshine' }
91+
}
92+
93+
return null;
94+
}
95+
}

src/main/resources/META-INF/plugin.xml

+3
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@
267267
<notificationGroup id="Symfony Notifications" displayType="STICKY_BALLOON"/>
268268
<errorHandler implementation="fr.adrienbrault.idea.symfony2plugin.util.ide.SymfonyPluginErrorReporterSubmitter"/>
269269

270+
<runLineMarkerContributor language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandTestRunLineMarkerProvider"/>
271+
<runConfigurationProducer implementation="fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandLazyRunConfigurationProducer"/>
272+
270273
<localInspection groupPath="Symfony" shortName="PhpRouteMissingInspection" displayName="Route Missing"
271274
groupName="Route"
272275
enabledByDefault="true" level="WARNING"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.dic.command;
2+
3+
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
4+
import com.jetbrains.php.lang.psi.elements.PhpClass;
5+
import fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandTestRunLineMarkerProvider;
6+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
7+
8+
/**
9+
* @author Daniel Espendiller <daniel@espendiller.net>
10+
* @see SymfonyCommandTestRunLineMarkerProvider
11+
*/
12+
public class SymfonyCommandTestRunLineMarkerProviderTest extends SymfonyLightCodeInsightFixtureTestCase {
13+
public void setUp() throws Exception {
14+
super.setUp();
15+
myFixture.copyFileToProject("classes.php");
16+
}
17+
18+
public String getTestDataPath() {
19+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/command/fixtures";
20+
}
21+
22+
public void testCommandNameFromDefaultNameProperty() {
23+
PhpClass phpClass = PhpPsiElementFactory.createFromText(getProject(), PhpClass.class, "<?php\n" +
24+
"class FoobarCommand extends \\Symfony\\Component\\Console\\Command\\Command {\n" +
25+
" protected static $defaultName = 'app:create-user';\n" +
26+
"}"
27+
);
28+
29+
assertEquals("app:create-user", SymfonyCommandTestRunLineMarkerProvider.getCommandNameFromClass(phpClass));
30+
}
31+
32+
public void testCommandNameFromDefaultPhpProperty() {
33+
PhpClass phpClass = PhpPsiElementFactory.createFromText(getProject(), PhpClass.class, "<?php\n" +
34+
"use Symfony\\Component\\Console\\Attribute\\AsCommand;\n" +
35+
"\n" +
36+
"#[AsCommand(\n" +
37+
" name: 'app:create-user',\n" +
38+
" description: 'Creates a new user.',\n" +
39+
" hidden: false,\n" +
40+
" aliases: ['app:add-user']\n" +
41+
")]\n" +
42+
"class FoobarCommand extends \\Symfony\\Component\\Console\\Command\\Command {\n" +
43+
"}"
44+
);
45+
46+
assertEquals("app:create-user", SymfonyCommandTestRunLineMarkerProvider.getCommandNameFromClass(phpClass));
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Symfony\Component\Console\Command
4+
{
5+
class Command
6+
{
7+
}
8+
}
9+
10+
namespace Symfony\Component\Console\Attribute
11+
{
12+
13+
/**
14+
* Service tag to autoconfigure commands.
15+
*/
16+
#[\Attribute(\Attribute::TARGET_CLASS)]
17+
class AsCommand
18+
{
19+
public function __construct(
20+
public string $name,
21+
public ?string $description = null,
22+
array $aliases = [],
23+
bool $hidden = false,
24+
) {}
25+
}
26+
}

0 commit comments

Comments
 (0)