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