diff --git a/platform/lang-impl/src/com/intellij/execution/impl/ConsoleViewImpl.java b/platform/lang-impl/src/com/intellij/execution/impl/ConsoleViewImpl.java index 6e1404ff3092..3299548d1bce 100644 --- a/platform/lang-impl/src/com/intellij/execution/impl/ConsoleViewImpl.java +++ b/platform/lang-impl/src/com/intellij/execution/impl/ConsoleViewImpl.java @@ -101,16 +101,15 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo public static final Key CONSOLE_VIEW_IN_EDITOR_VIEW = Key.create("CONSOLE_VIEW_IN_EDITOR_VIEW"); private static final Key CONTENT_TYPE = Key.create("ConsoleViewContentType"); + private static final Key USER_INPUT_SENT = Key.create("USER_INPUT_SENT"); private static boolean ourTypedHandlerInitialized; private final Alarm myFlushUserInputAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this); - private final StringBuilder myUserInputText = new StringBuilder(); // guarded by LOCK private static final CharMatcher NEW_LINE_MATCHER = CharMatcher.anyOf("\n\r"); private static synchronized void initTypedHandler() { if (ourTypedHandlerInitialized) return; - final EditorActionManager actionManager = EditorActionManager.getInstance(); - final TypedAction typedAction = actionManager.getTypedAction(); + TypedAction typedAction = EditorActionManager.getInstance().getTypedAction(); typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler())); ourTypedHandlerInitialized = true; } @@ -521,6 +520,8 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo void waitAllRequests() { myFlushAlarm.flush(); myFlushUserInputAlarm.flush(); + myFlushAlarm.flush(); + myFlushUserInputAlarm.flush(); } protected void disposeEditor() { @@ -564,7 +565,6 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo myDeferredBuffer.print(text, contentType, info); if (contentType == ConsoleViewContentType.USER_INPUT) { - sendUserInput(text); requestFlushImmediately(); } if (myEditor != null) { @@ -574,17 +574,29 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo } } - private void sendUserInput(@NotNull String text) { - if (myState.isRunning()) { - myUserInputText.append(text); - if (NEW_LINE_MATCHER.indexIn(text) >= 0) { - String textToSend = myUserInputText.toString(); - myUserInputText.setLength(0); + private void sendUserInput(@NotNull CharSequence typedText) { + if (myState.isRunning() && NEW_LINE_MATCHER.indexIn(typedText) >= 0) { + StringBuilder textToSend = new StringBuilder(); + // compute text input from the console contents: + // all range markers beginning from the caret offset backwards, marked as user input and not marked as already sent + for (RangeMarker marker = findTokenMarker(myEditor.getCaretModel().getOffset()); + marker != null; + marker = ((RangeMarkerImpl)marker).findRangeMarkerBefore()) { + ConsoleViewContentType tokenType = getTokenType(marker); + if (tokenType != null) { + if (tokenType != ConsoleViewContentType.USER_INPUT || marker.getUserData(USER_INPUT_SENT) == Boolean.TRUE) { + break; + } + marker.putUserData(USER_INPUT_SENT, true); + textToSend.insert(0, marker.getDocument().getText(TextRange.create(marker))); + } + } + if (textToSend.length() != 0) { myFlushUserInputAlarm.addRequest(() -> { if (myState.isRunning()) { try { // this may block forever, see IDEA-54340 - myState.sendUserInput(textToSend); + myState.sendUserInput(textToSend.toString()); } catch (IOException ignored) { } @@ -713,6 +725,7 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo if (shouldStickToEnd) { scrollToEnd(); } + sendUserInput(addedText); } private void createTokenRangeMarker(@NotNull Document document, @@ -1111,7 +1124,7 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo return marker[0]; } - private static ConsoleViewContentType getTokenType(RangeMarker m) { + private static ConsoleViewContentType getTokenType(@Nullable RangeMarker m) { return m == null ? null : m.getUserData(CONTENT_TYPE); } @@ -1498,6 +1511,7 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo } moveScrollRemoveSelection(editor, newEndOffset); + sendUserInput(text); } private void replaceUserText(int start, int end, @NotNull String text) { @@ -1506,8 +1520,10 @@ public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableCo final Document document = editor.getDocument(); document.replaceString(start, end, text); + int offset = start + text.length(); moveScrollRemoveSelection(editor, offset); + sendUserInput(text); } private static void moveScrollRemoveSelection(@NotNull Editor editor, int offset) { diff --git a/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java b/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java index f4092e20490e..f38c741bae28 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/execution/impl/ConsoleViewImplTest.java @@ -27,9 +27,9 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.actionSystem.EditorActionManager; import com.intellij.openapi.editor.actionSystem.TypedAction; +import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream; import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.testFramework.LightPlatformCodeInsightTestCase; @@ -42,13 +42,10 @@ import com.intellij.util.TimeoutUtil; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; -import java.io.InputStream; -import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; @@ -271,42 +268,9 @@ public class ConsoleViewImplTest extends LightPlatformTestCase { } public void testUserInputIsSentToProcessAfterNewLinePressed() { - byte[] buffer = new byte[10000]; - BufferExposingByteArrayOutputStream outputStream = new BufferExposingByteArrayOutputStream(buffer); - BufferExposingByteArrayInputStream inputStream = new BufferExposingByteArrayInputStream(buffer); - AtomicBoolean finished = new AtomicBoolean(); - Process testProcess = new Process() { - @Override - public OutputStream getOutputStream() { - return outputStream; - } + Process testProcess = AnsiEscapeDecoderTest.createTestProcess(); + BufferExposingByteArrayOutputStream outputStream = (BufferExposingByteArrayOutputStream)testProcess.getOutputStream(); - @Override - public InputStream getInputStream() { - return inputStream; - } - - @Override - public InputStream getErrorStream() { - return inputStream; - } - - @Override - public int waitFor() { - while (!finished.get()); - return 0; - } - - @Override - public int exitValue() { - return 0; - } - - @Override - public void destroy() { - finished.set(true); - } - }; AnsiEscapeDecoderTest.withProcessHandlerFrom(testProcess, handler -> withCycleConsole(100, console -> { console.attachToProcess(handler); @@ -323,4 +287,35 @@ public class ConsoleViewImplTest extends LightPlatformTestCase { assertEquals("IK\n", outputStream.toString()); })); } + + public void testUserTypingIsSentToProcessAfterNewLinePressed() { + Process testProcess = AnsiEscapeDecoderTest.createTestProcess(); + BufferExposingByteArrayOutputStream outputStream = (BufferExposingByteArrayOutputStream)testProcess.getOutputStream(); + + AnsiEscapeDecoderTest.withProcessHandlerFrom(testProcess, handler -> + withCycleConsole(100, console -> { + console.attachToProcess(handler); + outputStream.reset(); + Editor editor = console.getEditor(); + typeIn(editor, 'X'); + console.waitAllRequests(); + assertEquals(0, outputStream.size()); + + typeIn(editor, 'Y'); + console.waitAllRequests(); + assertEquals(0, outputStream.size()); + + typeIn(editor, '\n'); + console.waitAllRequests(); + assertEquals(3, outputStream.size()); + assertEquals("XY\n", outputStream.toString()); + })); + } + + private static void typeIn(Editor editor, char c) { + TypedAction action = EditorActionManager.getInstance().getTypedAction(); + DataContext dataContext = ((EditorEx)editor).getDataContext(); + + action.actionPerformed(editor, c, dataContext); + } } diff --git a/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java b/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java index 13687c20b01c..f8ee56345ae0 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java +++ b/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java @@ -100,12 +100,13 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase { }; } - public void testPerformance() throws IOException { - byte[] buffer = new byte[100000]; + @NotNull + public static Process createTestProcess() { + byte[] buffer = new byte[1000]; BufferExposingByteArrayOutputStream outputStream = new BufferExposingByteArrayOutputStream(buffer); BufferExposingByteArrayInputStream inputStream = new BufferExposingByteArrayInputStream(buffer); AtomicBoolean finished = new AtomicBoolean(); - Process testProcess = new Process() { + return new Process() { @Override public OutputStream getOutputStream() { return outputStream; @@ -137,6 +138,10 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase { finished.set(true); } }; + } + + public void testPerformance() throws IOException { + Process testProcess = createTestProcess(); withProcessHandlerFrom(testProcess, handler -> { PlatformTestUtil.startPerformanceTest("ansi color", 15000, ()->{ @@ -148,14 +153,14 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase { }); } - public static void withProcessHandlerFrom(@NotNull Process testProcess, @NotNull Consumer consumer) { + public static void withProcessHandlerFrom(@NotNull Process testProcess, @NotNull Consumer actionToTest) { KillableColoredProcessHandler handler = new KillableColoredProcessHandler(testProcess, "testProcess"); handler.setShouldDestroyProcessRecursively(false); handler.startNotify(); handler.notifyTextAvailable("Running stuff...\n", ProcessOutputTypes.STDOUT); try { - consumer.consume(handler); + actionToTest.consume(handler); } finally { handler.doDestroyProcess();