Skip to content

Commit d59ad90

Browse files
committed
Add code completion for Symfony console command names in the integrated terminal
1 parent f9a9dcb commit d59ad90

File tree

6 files changed

+484
-5
lines changed

6 files changed

+484
-5
lines changed

build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
"com.jetbrains.php",
3939
"com.jetbrains.twig",
4040
"com.jetbrains.php.dql",
41+
"org.jetbrains.plugins.terminal",
4142
"de.espend.idea.php.annotation",
4243
"de.espend.idea.php.toolbox"
4344
)
@@ -61,11 +62,13 @@ intellijPlatform {
6162
name = properties("pluginName")
6263
}
6364

65+
buildSearchableOptions = false
66+
6467
pluginVerification {
6568
ides {
6669
create(type, version) {
6770
useInstaller = false
68-
useCache = true
71+
useCache = false
6972
}
7073
}
7174
}

gradle.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pluginUntilBuild = 299.*
1515

1616
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
1717
platformType = IU
18-
platformVersion = 2025.2.5
18+
platformVersion = 2025.3
1919

2020
# Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3
2121
javaVersion = 21
@@ -29,4 +29,5 @@ gradleVersion = 9.2.1
2929
kotlin.stdlib.default.dependency = false
3030

3131

32-
org.gradle.configuration-cache=true
32+
org.gradle.configuration-cache=false
33+
kotlin.daemon.jvmargs=-Xmx2g
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package fr.adrienbrault.idea.symfony2plugin.completion.command;
2+
3+
import com.intellij.codeInsight.completion.*;
4+
import com.intellij.codeInsight.lookup.LookupElement;
5+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
6+
import com.intellij.openapi.editor.Editor;
7+
import com.intellij.openapi.project.DumbAware;
8+
import com.intellij.openapi.project.Project;
9+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
10+
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil;
11+
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyCommand;
12+
import org.jetbrains.annotations.NotNull;
13+
import org.jetbrains.plugins.terminal.block.util.TerminalDataContextUtils;
14+
import org.jetbrains.plugins.terminal.view.TerminalOffset;
15+
import org.jetbrains.plugins.terminal.view.TerminalOutputModel;
16+
import org.jetbrains.plugins.terminal.view.shellIntegration.TerminalBlockBase;
17+
import org.jetbrains.plugins.terminal.view.shellIntegration.TerminalBlocksModel;
18+
import org.jetbrains.plugins.terminal.view.shellIntegration.TerminalCommandBlock;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
/**
24+
* Provides code completion for Symfony console command names in the integrated terminal.
25+
*
26+
* <p>This completion contributor activates when typing Symfony console commands in the terminal,
27+
* detecting patterns like {@code bin/console <caret>} or {@code console <caret>} and offering
28+
* autocompletion suggestions for available Symfony commands.</p>
29+
*
30+
* <h3>Examples:</h3>
31+
* <pre>
32+
* # Terminal input → Completion result
33+
* $ bin/console <caret> → Shows all available commands
34+
* $ console cac<caret> → Filters to cache:* commands
35+
* $ bin/console debug:con<caret> → Suggests debug:container, debug:config, etc.
36+
* </pre>
37+
*/
38+
public class CommandNameTerminalCompletionContributor extends CompletionContributor implements DumbAware {
39+
40+
@Override
41+
public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
42+
// Only handle basic completion
43+
if (parameters.getCompletionType() != CompletionType.BASIC) {
44+
return;
45+
}
46+
47+
Editor editor = parameters.getEditor();
48+
Project project = editor.getProject();
49+
if (project == null) {
50+
return;
51+
}
52+
53+
if (!TerminalDataContextUtils.INSTANCE.isReworkedTerminalEditor(editor)) {
54+
return;
55+
}
56+
57+
// Check if this is a terminal editor
58+
TerminalOutputModel outputModel = editor.getUserData(TerminalOutputModel.Companion.getKEY());
59+
if (outputModel == null) {
60+
return;
61+
}
62+
63+
TerminalBlocksModel blocksModel = editor.getUserData(TerminalBlocksModel.Companion.getKEY());
64+
if (blocksModel == null) {
65+
return;
66+
}
67+
68+
// Get the active command block
69+
TerminalBlockBase activeBlock = blocksModel.getActiveBlock();
70+
if (!(activeBlock instanceof TerminalCommandBlock commandBlock)) {
71+
return;
72+
}
73+
74+
TerminalOffset commandStartOffset = commandBlock.getCommandStartOffset();
75+
if (commandStartOffset == null) {
76+
return;
77+
}
78+
79+
// Get the command text from the start of the command to the caret
80+
int caretOffset = editor.getCaretModel().getOffset();
81+
String commandText = outputModel.getText(
82+
commandStartOffset,
83+
outputModel.getStartOffset().plus(caretOffset)
84+
).toString();
85+
86+
// Check if the command starts with "bin/console" or "console"
87+
if (!SymfonyCommandUtil.isSymfonyConsoleCommand(commandText)) {
88+
return;
89+
}
90+
91+
// Extract the prefix for completion
92+
String prefix = extractCompletionPrefix(commandText);
93+
94+
// Update result set with a custom prefix matcher
95+
CompletionResultSet customResult = result.withPrefixMatcher(new PlainPrefixMatcher(prefix, true));
96+
97+
customResult.addAllElements(getSymfonyCommandSuggestions(editor.getProject()));
98+
}
99+
100+
private String extractCompletionPrefix(String commandText) {
101+
String trimmed = commandText.trim();
102+
103+
// Remove "bin/console " or "console " prefix
104+
if (trimmed.startsWith("bin/console ")) {
105+
trimmed = trimmed.substring("bin/console ".length());
106+
} else if (trimmed.startsWith("console ")) {
107+
trimmed = trimmed.substring("console ".length());
108+
} else {
109+
return "";
110+
}
111+
112+
// For now, we only support command name completion (not arguments)
113+
// So we take everything until the first space
114+
int spaceIndex = trimmed.indexOf(' ');
115+
if (spaceIndex > 0) {
116+
// Already typed a complete command with arguments, no completion
117+
return "";
118+
}
119+
120+
return trimmed;
121+
}
122+
123+
private List<LookupElement> getSymfonyCommandSuggestions(@NotNull Project project) {
124+
List<LookupElement> suggestions = new ArrayList<>();
125+
126+
for (SymfonyCommand command : SymfonyCommandUtil.getCommands(project)) {
127+
String className = extractClassName(command.getFqn());
128+
suggestions.add(LookupElementBuilder.create(command.getName())
129+
.withTypeText(className, true)
130+
.withIcon(Symfony2Icons.SYMFONY)
131+
);
132+
}
133+
134+
return suggestions;
135+
}
136+
137+
private String extractClassName(@NotNull String fqn) {
138+
int lastBackslash = fqn.lastIndexOf('\\');
139+
140+
if (lastBackslash >= 0 && lastBackslash < fqn.length() - 1) {
141+
return fqn.substring(lastBackslash + 1);
142+
}
143+
144+
return fqn;
145+
}
146+
}

0 commit comments

Comments
 (0)