[terminal] IJPL-211122 Document TerminalShellIntegration and related interfaces

(cherry picked from commit e8722fa85d0ff51d574d60c4fa9e3c9eb3a0f886)

IJ-CR-180081

GitOrigin-RevId: bfcb3cfa5212cbadd6e4f4fb370c2d50fc732794
This commit is contained in:
Konstantin Hudyakov
2025-10-27 20:48:27 +00:00
committed by intellij-monorepo-bot
parent 34a20b3806
commit d6e1c5df15
8 changed files with 174 additions and 9 deletions
@@ -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<TerminalShellIntegration>
@@ -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
@@ -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:
* ```
* <startOffset>prompt: <commandStartOffset>some command
* <outputStartOffset>some
* command
* output
* <endOffset>
* ```
*
* 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
@@ -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<TerminalBlockBase>
/**
* 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)
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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<TerminalOutputStatus>
/**
* Allows listening for command start and finish events.
*/
fun addCommandExecutionListener(parentDisposable: Disposable, listener: TerminalCommandExecutionListener)
}