mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
terminal: support TerminalWidget.sendCommandToExecute for not-yet-started sessions (IDEA-347591)
Now, command block creation is postponed until previous command is finished. GitOrigin-RevId: 1fe0435338e34d5ea073a3c3882418d54ea0e8a0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1a5018dacb
commit
8f303a5d28
@@ -50,12 +50,18 @@ class BlockTerminalController(
|
||||
promptController.reset()
|
||||
outputController.insertEmptyLine()
|
||||
}
|
||||
else startCommand(command)
|
||||
else {
|
||||
session.commandManager.sendCommandToExecute(command)
|
||||
outputController.doWhenNextBlockCanBeStarted {
|
||||
startCommandBlock(command)
|
||||
}
|
||||
}
|
||||
// report event even if it is an empty command, because it will be reported as a separate command type
|
||||
TerminalUsageTriggerCollector.triggerCommandExecuted(project, command, isBlockTerminal = true)
|
||||
}
|
||||
|
||||
private fun startCommand(command: String) {
|
||||
@RequiresEdt(generateAssertion = false)
|
||||
private fun startCommandBlock(command: String) {
|
||||
outputController.startCommandBlock(command, promptController.promptRenderingInfo)
|
||||
// Hide the prompt only when the new block is created, so it will look like the prompt is replaced with a block atomically.
|
||||
// If the command is finished very fast, the prompt will be shown back before repainting.
|
||||
@@ -67,7 +73,6 @@ class BlockTerminalController(
|
||||
Disposer.dispose(disposable)
|
||||
}
|
||||
}, disposable)
|
||||
session.commandManager.sendCommandToExecute(command)
|
||||
session.model.isCommandRunning = true
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jediterm.terminal.TextStyle
|
||||
import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.IS_OUTPUT_EDITOR_KEY
|
||||
import org.jetbrains.plugins.terminal.exp.hyperlinks.TerminalHyperlinkHighlighter
|
||||
import java.util.*
|
||||
|
||||
class TerminalOutputController(
|
||||
project: Project,
|
||||
@@ -37,6 +38,8 @@ class TerminalOutputController(
|
||||
|
||||
private val hyperlinkHighlighter: TerminalHyperlinkHighlighter = TerminalHyperlinkHighlighter(project, outputModel, session)
|
||||
|
||||
private val nextBlockCanBeStartedQueue: Queue<() -> Unit> = LinkedList()
|
||||
|
||||
init {
|
||||
editor.putUserData(IS_OUTPUT_EDITOR_KEY, true)
|
||||
editor.highlighter = textHighlighter
|
||||
@@ -112,6 +115,7 @@ class TerminalOutputController(
|
||||
outputModel.finalizeBlock(block)
|
||||
}
|
||||
runningCommandContext = null
|
||||
nextBlockCanBeStartedQueue.poll()?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +277,16 @@ class TerminalOutputController(
|
||||
else editor.document.addDocumentListener(listener)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun doWhenNextBlockCanBeStarted(callback: () -> Unit) {
|
||||
if (runningCommandContext == null && outputModel.getActiveBlock() == null) {
|
||||
callback()
|
||||
}
|
||||
else {
|
||||
nextBlockCanBeStartedQueue.offer(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private data class CommandOutput(val text: String, val highlightings: List<HighlightingInfo>)
|
||||
|
||||
private data class RunningCommandContext(val command: String?, val prompt: PromptRenderingInfo?)
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.plugins.terminal.block
|
||||
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.terminal.TerminalTitle
|
||||
import com.intellij.testFramework.*
|
||||
import com.jediterm.core.util.TermSize
|
||||
import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider
|
||||
import org.jetbrains.plugins.terminal.exp.BlockTerminalView
|
||||
import org.jetbrains.plugins.terminal.exp.CommandBlock
|
||||
import org.jetbrains.plugins.terminal.exp.TerminalOutputModel
|
||||
import org.jetbrains.plugins.terminal.exp.util.TerminalSessionTestUtil
|
||||
import org.jetbrains.plugins.terminal.exp.util.TerminalSessionTestUtil.toCommandLine
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunsInEdt
|
||||
@RunWith(Parameterized::class)
|
||||
class BlockTerminalCommandExecutionTest(private val shellPath: Path) {
|
||||
|
||||
private val projectRule: ProjectRule = ProjectRule()
|
||||
private val disposableRule = DisposableRule()
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun shells(): List<Path> = TerminalSessionTestUtil.getShellPaths()
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleChain: RuleChain = RuleChain(EdtRule(), projectRule, disposableRule)
|
||||
|
||||
@Test
|
||||
fun `commands are executed in order`() {
|
||||
val session = startBlockTerminalSession()
|
||||
val view = BlockTerminalView(projectRule.project, session, JBTerminalSystemSettingsProvider(), TerminalTitle())
|
||||
Disposer.register(disposableRule.disposable, view)
|
||||
|
||||
view.outputView.controller.finishCommandBlock(0) // emulate `initialized` event as it's consumed in `startBlockTerminalSession()`
|
||||
val count = 50
|
||||
val expected = (1..count).map {
|
||||
val message = "Hello, World $it"
|
||||
CommandResult(listOf("echo", message).toCommandLine(session), message)
|
||||
}
|
||||
expected.forEach {
|
||||
view.sendCommandToExecute(it.command)
|
||||
}
|
||||
awaitBlocksFinalized(view.outputView.controller.outputModel, count)
|
||||
val actual = view.outputView.controller.outputModel.collectCommandResults()
|
||||
Assert.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
private fun awaitBlocksFinalized(outputModel: TerminalOutputModel, commandBlocks: Int, duration: Duration = 20.seconds) {
|
||||
val latch = CountDownLatch(commandBlocks)
|
||||
outputModel.addListener(object : TerminalOutputModel.TerminalOutputListener {
|
||||
override fun blockFinalized(block: CommandBlock) {
|
||||
if (block.withCommand) {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
})
|
||||
val error = { "Timed out waiting for command blocks: finished: ${commandBlocks - latch.count}, expected: $commandBlocks" }
|
||||
PlatformTestUtil.waitWithEventsDispatching(error, { latch.count.toInt() == 0 }, duration.inWholeSeconds.toInt())
|
||||
}
|
||||
|
||||
private fun startBlockTerminalSession(termSize: TermSize = TermSize(80, 24)) =
|
||||
TerminalSessionTestUtil.startBlockTerminalSession(projectRule.project, shellPath.toString(), disposableRule.disposable, termSize)
|
||||
|
||||
}
|
||||
|
||||
private data class CommandResult(val command: String, val output: String)
|
||||
|
||||
private fun TerminalOutputModel.collectCommandResults(): List<CommandResult> {
|
||||
return (0 until getBlocksSize()).mapNotNull {
|
||||
val commandBlock = this.getByIndex(it)
|
||||
check(commandBlock.isFinalized)
|
||||
val command = commandBlock.command
|
||||
if (it == 0 && command == null) {
|
||||
null // skip the initial block
|
||||
}
|
||||
else {
|
||||
CommandResult(command!!, editor.document.getText(TextRange(commandBlock.outputStartOffset, commandBlock.endOffset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -159,6 +159,20 @@ object TerminalSessionTestUtil {
|
||||
this.sendCommandlineToExecuteWithoutAddingToHistory(commandline)
|
||||
}
|
||||
|
||||
fun List<String>.toCommandLine(session: BlockTerminalSession, preventAddingToHistory: Boolean = true): String {
|
||||
return when (session.shellIntegration.shellType) {
|
||||
ShellType.POWERSHELL -> {
|
||||
// saving command history is disabled for PowerShell in [disableSavingHistory]
|
||||
toPowerShellCommandLine(this)
|
||||
}
|
||||
else -> {
|
||||
val commandLine = toPosixShellCommandLine(this)
|
||||
// prefix with a space to prevent adding it to history for Zsh/Bash/fish (works by default)
|
||||
if (preventAddingToHistory) " $commandLine" else commandLine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BlockTerminalSession.sendCommandlineToExecuteWithoutAddingToHistory(shellCommandline: String) {
|
||||
if (this.shellIntegration.shellType == ShellType.POWERSHELL) {
|
||||
// saving command history is disabled for PowerShell in [disableSavingHistory]
|
||||
|
||||
Reference in New Issue
Block a user