From cdd6ec35d9082e1d5f9dd49d4f08f4232bcb489d Mon Sep 17 00:00:00 2001 From: Konstantin Hudyakov Date: Wed, 1 Jul 2026 14:46:18 +0300 Subject: [PATCH] [terminal] IJPL-191456 Fix inconsistency in blocks model `startNewBlock` is called before shell prints the prompt. It looks like sometimes (theoretically, no actual steps to reproduce), prompt position can be before the previous block start, so our code adjusts the current block by setting endOffset < startOffset. It is a single reason that may explain the fact that "startOffset" is greater than the "endOffset" in the exception reports. Let's add protection for this case - remove blocks after the cursor offset in this case. (cherry picked from commit 3f47dc01d890f37729d2c780538f70412884f321) IJ-CR-211622 GitOrigin-RevId: 2859d1672de099c02c2628fea871d0a2c0554fea --- .../impl/TerminalBlocksModelImpl.kt | 8 +++++ .../tests/reworked/TerminalBlocksModelTest.kt | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/impl/TerminalBlocksModelImpl.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/impl/TerminalBlocksModelImpl.kt index a3fdc47a6222..77e32384e283 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/impl/TerminalBlocksModelImpl.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/impl/TerminalBlocksModelImpl.kt @@ -63,6 +63,14 @@ class TerminalBlocksModelImpl( } fun startNewBlock(offset: TerminalOffset) { + // The new prompt may start before the active block (or even before some already finished blocks) + // if the shell moved the cursor backwards (e.g. the screen was redrawn or partially cleared via + // cursor movement, a transient prompt was printed, or the output was reflowed on resize). + // Remove all blocks that start after the new prompt offset, since they are being overwritten. + // This also keeps the remaining active block's start offset <= offset, so the block below can + // never end up with an inverted range (startOffset > endOffset). + trimBlocksAfter(offset) + val active = activeBlock as TerminalCommandBlockImpl if (offset == active.startOffset) { blocks.removeLast() diff --git a/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/TerminalBlocksModelTest.kt b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/TerminalBlocksModelTest.kt index 3bdc82545f4b..6f3f19b1fbe9 100644 --- a/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/TerminalBlocksModelTest.kt +++ b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/TerminalBlocksModelTest.kt @@ -328,6 +328,40 @@ internal class TerminalBlocksModelTest : BasePlatformTestCase() { assertEquals("updatedPrompt: \n", outputModel.getTextAsString(secondBlock.startOffset, secondBlock.endOffset)) } + @Test + fun `new prompt started before the active block does not create an inverted block`() = runBlocking(Dispatchers.EDT) { + val outputModel = TerminalTestUtil.createOutputModel() + val blocksModel = createBlocksModel(outputModel) + + // Prepare a finished block [0, 31) and an active block [31, 47). + outputModel.update(0, "\n\n\n") + blocksModel.startNewBlock(TerminalOffset.ZERO) + outputModel.update(0, "myPrompt: \n\n\n") + blocksModel.updateCommandStartOffset(TerminalOffset.of(10)) + outputModel.update(0, "myPrompt: myCommand\n\n\n") + blocksModel.updateOutputStartOffset(TerminalOffset.of(20)) + outputModel.update(1, "someOutput\n\n") + blocksModel.startNewBlock(TerminalOffset.of(31)) + outputModel.update(2, "updatedPrompt: \n") + + assertEquals(2, blocksModel.blocks.size) + + // The shell moves the cursor backwards into the first (finished) block and starts a new prompt there. + // Previously this set the active block end offset below its start offset, producing an inverted range (startOffset > endOffset). + blocksModel.startNewBlock(TerminalOffset.of(15)) + + assertEquals(2, blocksModel.blocks.size) + + // The overwritten block [31, 47) is gone; the first block is trimmed to end at the new prompt offset. + val firstBlock = blocksModel.blocks[0] as TerminalCommandBlock + assertEquals(TerminalOffset.ZERO, firstBlock.startOffset) + assertEquals(TerminalOffset.of(15), firstBlock.endOffset) + + val secondBlock = blocksModel.blocks[1] as TerminalCommandBlock + assertEquals(TerminalOffset.of(15), secondBlock.startOffset) + assertEquals(TerminalOffset.of(47), secondBlock.endOffset) + } + @Test fun `initial block is left if there was some text`() = runBlocking(Dispatchers.EDT) { val outputModel = TerminalTestUtil.createOutputModel()