diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt index ff2132564aa7..778d400f6011 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/view/TerminalView.kt @@ -70,10 +70,14 @@ interface TerminalView { /** * Can be used to get or await the shell integration initialization. - * Note that it may never complete because the shell integration may be not available + * + * Note that **it may never complete** because the shell integration may be not available * (for example, because of an unsupported shell or environment) * or it can be disabled in the Terminal settings ([org.jetbrains.plugins.terminal.TerminalOptionsProvider.shellIntegration]). * + * If the started shell supports the shell integration, + * it will be initialized before the first prompt is printed in the output. + * * @see TerminalShellIntegration */ val shellIntegrationDeferred: Deferred diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlockId.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlockId.kt index 5bff1d939855..a0ee18b51af4 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlockId.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlockId.kt @@ -4,6 +4,9 @@ package org.jetbrains.plugins.terminal.view.shellIntegration import kotlinx.serialization.Serializable import org.jetbrains.annotations.ApiStatus +/** + * The unique identifier of the terminal block. + */ @ApiStatus.Experimental @Serializable sealed interface TerminalBlockId diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocks.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocks.kt index ea8e842b600d..a59fc383a1cf 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocks.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocks.kt @@ -5,26 +5,92 @@ import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.terminal.view.TerminalOffset import org.jetbrains.plugins.terminal.view.TerminalOutputModel +/** + * The terminal block is the range of text in the [TerminalOutputModel] and some additional information + * about the content and meaning of this text. + * + * Currently, there is a single implementation: [TerminalCommandBlock]. + * So, use safe cast to [TerminalCommandBlock] if you need to work with the command block. + * But other implementations might be added in the future. + * + * Blocks are supported only for the [regular][org.jetbrains.plugins.terminal.view.TerminalOutputModelsSet.regular] output model, + * so all [TerminalOffset]'s are specified relative to it. + */ @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalBlockBase { val id: TerminalBlockId + + /** + * Always check that this offset is in the bounds of the regular [TerminalOutputModel] before accessing the text. + * Because when the output model starts trimming the start of the output (because of reaching the max length), + * the block offsets are not updated. + */ val startOffset: TerminalOffset + val endOffset: TerminalOffset } +/** + * The terminal block that represents the range of the terminal output that can contain + * prompt, command and the command output. + * Also, it provides additional metadata about the command, such as [workingDirectory], [executedCommand] and [exitCode]. + * + * The usual structure of the block is the following: + * ``` + * prompt: some command + * some + * command + * output + * + * ``` + * + * Note that the shell output can also contain the right prompt and line continuations. + * It is worth detecting the positions of these parts as well, but it is not supported at the moment. + */ @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalCommandBlock : TerminalBlockBase { + /** + * The offset where the prompt finishes and command text starts. + * Can be null in the initial block of the output before the first prompt is printed. + * + * Always check that this offset is in the bounds of the regular [TerminalOutputModel] before accessing the text. + * Because when the output model starts trimming the start of the output (because of reaching the max length), + * the block offsets are not updated. + */ val commandStartOffset: TerminalOffset? + + /** + * The offset where the command output starts. + * + * Can be null if the command was not started to execute. + * For example, if a user is typing a command now. + * Or if the command was meaningless and shell just printed the new prompt. + * Or if the user aborted the command typing by pressing Ctrl+C. + * + * Always check that this offset is in the bounds of the regular [TerminalOutputModel] before accessing the text. + * Because when the output model starts trimming the start of the output (because of reaching the max length), + * the block offsets are not updated. + */ val outputStartOffset: TerminalOffset? - val workingDirectory: String? /** - * Should be non-null if the command was started to execute. - * It is the command text reported by the shell integration right before it is started. + * The absolute OS-dependent path to the working directory that was set in a shell + * when this command block was active. + */ + val workingDirectory: String? + + /** + * The command text reported by the shell integration right before it is started. + * Null if the command was not executed. */ val executedCommand: String? + + /** + * The exit code reported by the shell integration when the command execution was finished. + * Null if the command was not executed. + */ val exitCode: Int? } @@ -54,7 +120,7 @@ fun TerminalCommandBlock.getTypedCommandText(model: TerminalOutputModel): String * @param model regular terminal output model (not alternative one) * @return command output text with possibly trimmed start and without trailing whitespaces. * Can return null if no command was running in this block or the block is out of model bounds. - * If the command produces no output, an empty string will be returned. + * If the command was executed but produced no output, an empty string will be returned. */ @ApiStatus.Experimental fun TerminalCommandBlock.getOutputText(model: TerminalOutputModel): String? { @@ -69,7 +135,7 @@ fun TerminalCommandBlock.getOutputText(model: TerminalOutputModel): String? { } /** - * @return true if the command was started to execute in this block, so it contains some output. + * @return true if the command was started to execute in this block, so it can contain some output. */ @get:ApiStatus.Experimental val TerminalCommandBlock.wasExecuted: Boolean diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModel.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModel.kt index 8459a3a6b6ea..8a0364218567 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModel.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModel.kt @@ -3,16 +3,39 @@ package org.jetbrains.plugins.terminal.view.shellIntegration import com.intellij.openapi.Disposable import com.intellij.openapi.util.Key -import com.intellij.util.concurrency.annotations.RequiresEdt import org.jetbrains.annotations.ApiStatus +/** + * The model that holds the information about the ranges of the shell output: terminal blocks. + * + * Note that the block model exists only in the context + * of the [regular][org.jetbrains.plugins.terminal.view.TerminalOutputModelsSet.regular] output model. + * So, all [org.jetbrains.plugins.terminal.view.TerminalOffset]'s are specified relative to it. + * + * The interface provides a read-only view, but the model itself is mutable and therefore should only be accessed on the mutating thread, + * which is currently the **EDT**. + * + * @see TerminalBlockBase + * @see TerminalCommandBlock + */ @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalBlocksModel { - /** The list can be mutable in the implementation, so it should not be cached. */ - @get:RequiresEdt + /** + * The list of the blocks in the model, it is always sorted in the order of appearance. + * So, the first block is the oldest in the output, and the last block is the newest. + * + * The list is always non-empty. + * The first block may be partially out of the regular [org.jetbrains.plugins.terminal.view.TerminalOutputModel] bounds due to trimming. + * + * The list can be mutable in the implementation, so it should not be cached. + */ val blocks: List + /** + * Currently active terminal block. + * For example, it can contain the current command a user is typing or executing. + */ val activeBlock: TerminalBlockBase fun addListener(parentDisposable: Disposable, listener: TerminalBlocksModelListener) diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModelListener.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModelListener.kt index 32ca22f94acc..b2332c2cfb03 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModelListener.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalBlocksModelListener.kt @@ -7,8 +7,17 @@ import org.jetbrains.annotations.ApiStatus interface TerminalBlocksModelListener { fun blockAdded(event: TerminalBlockAddedEvent) {} + /** + * Can be called when the block range becomes out of the regular [org.jetbrains.plugins.terminal.view.TerminalOutputModel] bounds + * because of trimming. + * Or when some text removing operation is performed, for example, executing `clear` command. + */ fun blockRemoved(event: TerminalBlockRemovedEvent) {} + /** + * Can be called when some mass text replacement operation is performed, for example, + * when initial state of the [TerminalBlocksModel] is received from the backend. + */ fun blocksReplaced(event: TerminalBlocksReplacedEvent) {} } @@ -21,12 +30,14 @@ interface TerminalBlocksModelEvent { @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalBlockAddedEvent : TerminalBlocksModelEvent { + /** The block that was added to the model */ val block: TerminalBlockBase } @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalBlockRemovedEvent : TerminalBlocksModelEvent { + /** The block that was removed from the model */ val block: TerminalBlockBase } diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalCommandExecutionListener.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalCommandExecutionListener.kt index 22659db57f0f..fba436ba08cf 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalCommandExecutionListener.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalCommandExecutionListener.kt @@ -6,25 +6,38 @@ import org.jetbrains.plugins.terminal.view.TerminalOutputModel @ApiStatus.Experimental interface TerminalCommandExecutionListener { + /** + * Called when the "Enter" key is received by the shell, the shell identified + * the typed command as a complete command and going to execute it. + */ fun commandStarted(event: TerminalCommandStartedEvent) {} + /** + * Called when shell finished executing the command. + * The output and other metadata can be accessed from the [TerminalCommandFinishedEvent.commandBlock]. + */ fun commandFinished(event: TerminalCommandFinishedEvent) {} } @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalCommandExecutionEvent { + /** + * The same as the [regular][org.jetbrains.plugins.terminal.view.TerminalOutputModelsSet.regular] output model. + */ val outputModel: TerminalOutputModel } @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalCommandStartedEvent : TerminalCommandExecutionEvent { + /** The block with the information about the executing command */ val commandBlock: TerminalCommandBlock } @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalCommandFinishedEvent : TerminalCommandExecutionEvent { + /** The block with the information about the finished command */ val commandBlock: TerminalCommandBlock } \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalOutputStatus.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalOutputStatus.kt index 05d61c8cfdbc..b48606c88fe9 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalOutputStatus.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalOutputStatus.kt @@ -3,10 +3,28 @@ package org.jetbrains.plugins.terminal.view.shellIntegration import org.jetbrains.annotations.ApiStatus +/** + * Represents the state of the terminal during shell output processing. + */ @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalOutputStatus { + /** + * The terminal is waiting until the shell prompt is received from the output. + * + * For example, this status is active when the shell is just started and didn't print the first prompt yet. + * Or when we start receiving the prompt text after the command execution. + */ object WaitingForPrompt : TerminalOutputStatus + + /** + * The prompt was received and user is invited to type the command. + */ object TypingCommand : TerminalOutputStatus + + /** + * User typed the command and pressed Enter to execute it. + * Now the output of the command is being received. + */ object ExecutingCommand : TerminalOutputStatus } \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalShellIntegration.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalShellIntegration.kt index 56bdb8658f30..523d693f1203 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalShellIntegration.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/view/shellIntegration/TerminalShellIntegration.kt @@ -5,12 +5,39 @@ import com.intellij.openapi.Disposable import kotlinx.coroutines.flow.StateFlow import org.jetbrains.annotations.ApiStatus +/** + * Declares the features provided by the terminal shell integration (when it is available). + * + * When we start the shell process, we inject our shell scripts into its startup. + * To get the information about the environment and subscribe for events. + * For example, to know the positions of the prompt, command and the command output in the shell output. + * + * Currently, the shell integration is available only for **Bash, Zsh and PowerShell**. + * But in Zsh the integration is disabled when the PowerLevel10K plugin is installed. + * Also, it is controlled by the option in the Terminal settings. + * ([org.jetbrains.plugins.terminal.TerminalOptionsProvider.shellIntegration]) + */ @ApiStatus.Experimental @ApiStatus.NonExtendable interface TerminalShellIntegration { + /** + * The model that holds the information ranges of the shell output. + * + * @see TerminalBlockBase + * @see TerminalCommandBlock + */ val blocksModel: TerminalBlocksModel + /** + * Represents the current state of the terminal during shell output processing. + * + * StateFlow can be useful to await some particular state in a suspending context, + * for example `outputStatus.first { it == TypingCommand }`. + */ val outputStatus: StateFlow + /** + * Allows listening for command start and finish events. + */ fun addCommandExecutionListener(parentDisposable: Disposable, listener: TerminalCommandExecutionListener) } \ No newline at end of file