terminal: trim command when matching command for smart execution and when smart execu...
[idea/community.git] / plugins / terminal / src / org / jetbrains / plugins / terminal / ShellTerminalWidget.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.plugins.terminal;
3
4 import com.intellij.openapi.Disposable;
5 import com.intellij.openapi.diagnostic.Logger;
6 import com.intellij.openapi.project.Project;
7 import com.intellij.openapi.util.text.StringUtil;
8 import com.intellij.terminal.JBTerminalPanel;
9 import com.intellij.terminal.JBTerminalSystemSettingsProviderBase;
10 import com.intellij.terminal.JBTerminalWidget;
11 import com.jediterm.terminal.ProcessTtyConnector;
12 import com.jediterm.terminal.Terminal;
13 import com.jediterm.terminal.TtyConnector;
14 import com.jediterm.terminal.model.TerminalLine;
15 import com.jediterm.terminal.model.TerminalTextBuffer;
16 import org.jetbrains.annotations.NotNull;
17 import org.jetbrains.annotations.Nullable;
18
19 import java.awt.event.KeyEvent;
20 import java.io.IOException;
21 import java.nio.charset.StandardCharsets;
22 import java.util.LinkedList;
23 import java.util.Queue;
24 import java.util.function.Function;
25
26 public class ShellTerminalWidget extends JBTerminalWidget {
27
28   private static final Logger LOG = Logger.getInstance(ShellTerminalWidget.class);
29
30   private final Project myProject;
31   private boolean myEscapePressed = false;
32   private String myCommandHistoryFilePath;
33   private boolean myPromptUpdateNeeded = true;
34   private String myPrompt = "";
35   private final Queue<String> myPendingCommandsToExecute = new LinkedList<>();
36   private final TerminalShellCommandHandlerHelper myShellCommandHandlerHelper;
37
38   public ShellTerminalWidget(@NotNull Project project,
39                              @NotNull JBTerminalSystemSettingsProviderBase settingsProvider,
40                              @NotNull Disposable parent) {
41     super(project, settingsProvider, parent);
42     myProject = project;
43     myShellCommandHandlerHelper = new TerminalShellCommandHandlerHelper(this);
44
45     ((JBTerminalPanel)getTerminalPanel()).addPreKeyEventHandler(e -> {
46       if (e.getID() != KeyEvent.KEY_PRESSED) return;
47       if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
48         myEscapePressed = true;
49       }
50       if (myPromptUpdateNeeded) {
51         myPrompt = getLineAtCursor();
52         if (LOG.isDebugEnabled()) {
53           LOG.info("Guessed shell prompt: " + myPrompt);
54         }
55         myPromptUpdateNeeded = false;
56       }
57
58       if (e.getKeyCode() == KeyEvent.VK_ENTER || TerminalShellCommandHandlerHelper.matchedExecutor(e) != null) {
59         TerminalUsageTriggerCollector.Companion.triggerCommandExecuted(myProject);
60         if (myShellCommandHandlerHelper.processEnterKeyPressed(e)) {
61           e.consume();
62         }
63         if (!e.isConsumed()) {
64           myPromptUpdateNeeded = true;
65           myEscapePressed = false;
66         }
67       }
68       else {
69         myShellCommandHandlerHelper.processKeyPressed();
70       }
71     });
72   }
73
74   @NotNull
75   Project getProject() {
76     return myProject;
77   }
78
79   public void setCommandHistoryFilePath(@Nullable String commandHistoryFilePath) {
80     myCommandHistoryFilePath = commandHistoryFilePath;
81   }
82
83   @Nullable
84   public static String getCommandHistoryFilePath(@Nullable JBTerminalWidget terminalWidget) {
85     return terminalWidget instanceof ShellTerminalWidget ? ((ShellTerminalWidget)terminalWidget).myCommandHistoryFilePath : null;
86   }
87
88   @NotNull
89   public String getTypedShellCommand() {
90     if (myPromptUpdateNeeded) {
91       return "";
92     }
93     String line = getLineAtCursor();
94     return StringUtil.trimStart(line, myPrompt);
95   }
96
97   private @NotNull String getLineAtCursor() {
98     return processTerminalBuffer(textBuffer -> {
99       TerminalLine line = textBuffer.getLine(getLineNumberAtCursor());
100       return line != null ? line.getText() : "";
101     });
102   }
103
104   <T> T processTerminalBuffer(@NotNull Function<TerminalTextBuffer, T> processor) {
105     TerminalTextBuffer textBuffer = getTerminalPanel().getTerminalTextBuffer();
106     textBuffer.lock();
107     try {
108       return processor.apply(textBuffer);
109     }
110     finally {
111       textBuffer.unlock();
112     }
113   }
114
115   int getLineNumberAtCursor() {
116     TerminalTextBuffer textBuffer = getTerminalPanel().getTerminalTextBuffer();
117     Terminal terminal = getTerminal();
118     return Math.max(0, Math.min(terminal.getCursorY() - 1, textBuffer.getHeight() - 1));
119   }
120
121   public void executeCommand(@NotNull String shellCommand) throws IOException {
122     String typedCommand = getTypedShellCommand();
123     if (!typedCommand.isEmpty()) {
124       throw new IOException("Cannot execute command when another command is typed: " + typedCommand); //NON-NLS
125     }
126     TtyConnector connector = getTtyConnector();
127     if (connector != null) {
128       doExecuteCommand(shellCommand, connector);
129     }
130     else {
131       myPendingCommandsToExecute.add(shellCommand);
132     }
133   }
134
135   @Override
136   public void setTtyConnector(@NotNull TtyConnector ttyConnector) {
137     super.setTtyConnector(ttyConnector);
138     String command;
139     while ((command = myPendingCommandsToExecute.poll()) != null) {
140       try {
141         doExecuteCommand(command, ttyConnector);
142       }
143       catch (IOException e) {
144         LOG.warn("Cannot execute " + command, e);
145       }
146     }
147   }
148
149   private void doExecuteCommand(@NotNull String shellCommand, @NotNull TtyConnector connector) throws IOException {
150     StringBuilder result = new StringBuilder();
151     if (myEscapePressed) {
152       result.append((char)KeyEvent.VK_BACK_SPACE); // remove Escape first, workaround for IDEA-221031
153     }
154     String enterCode = new String(getTerminalStarter().getCode(KeyEvent.VK_ENTER, 0), StandardCharsets.UTF_8);
155     result.append(shellCommand).append(enterCode);
156     connector.write(result.toString());
157   }
158
159   public boolean hasRunningCommands() throws IllegalStateException {
160     TtyConnector connector = getTtyConnector();
161     if (connector == null) return false;
162     if (connector instanceof ProcessTtyConnector) {
163       return TerminalUtil.hasRunningCommands((ProcessTtyConnector)connector);
164     }
165     throw new IllegalStateException("Cannot determine if there are running processes for " + connector.getClass()); //NON-NLS
166   }
167 }