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+ // If prefix is null, we're not in a valid position for command name completion
95+ // (e.g., we're typing arguments/options after the command name)
96+ if (prefix == null ) {
97+ return ;
98+ }
99+
100+ // Update result set with a custom prefix matcher
101+ CompletionResultSet customResult = result .withPrefixMatcher (new PlainPrefixMatcher (prefix , true ));
102+
103+ customResult .addAllElements (getSymfonyCommandSuggestions (editor .getProject ()));
104+ }
105+
106+ private String extractCompletionPrefix (String commandText ) {
107+ String trimmed = commandText .trim ();
108+
109+ // Remove "bin/console " or "console " prefix
110+ if (trimmed .startsWith ("bin/console " )) {
111+ trimmed = trimmed .substring ("bin/console " .length ());
112+ } else if (trimmed .startsWith ("console " )) {
113+ trimmed = trimmed .substring ("console " .length ());
114+ } else {
115+ return null ;
116+ }
117+
118+ // For now, we only support command name completion (not arguments)
119+ // So we take everything until the first space
120+ int spaceIndex = trimmed .indexOf (' ' );
121+ if (spaceIndex > 0 ) {
122+ // Already typed a complete command with arguments, no completion
123+ return null ;
124+ }
125+
126+ return trimmed ;
127+ }
128+
129+ private List <LookupElement > getSymfonyCommandSuggestions (@ NotNull Project project ) {
130+ List <LookupElement > suggestions = new ArrayList <>();
131+
132+ for (SymfonyCommand command : SymfonyCommandUtil .getCommands (project )) {
133+ String className = extractClassName (command .getFqn ());
134+ suggestions .add (LookupElementBuilder .create (command .getName ())
135+ .withTypeText (className , true )
136+ .withIcon (Symfony2Icons .SYMFONY )
137+ );
138+ }
139+
140+ return suggestions ;
141+ }
142+
143+ private String extractClassName (@ NotNull String fqn ) {
144+ int lastBackslash = fqn .lastIndexOf ('\\' );
145+
146+ if (lastBackslash >= 0 && lastBackslash < fqn .length () - 1 ) {
147+ return fqn .substring (lastBackslash + 1 );
148+ }
149+
150+ return fqn ;
151+ }
152+ }
0 commit comments