Skip to content

Commit

Permalink
Stub Terminal Shell Integration VS Code API (#14058)
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew authored Aug 21, 2024
1 parent 5d51320 commit 5af9da6
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 5 deletions.
12 changes: 10 additions & 2 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ import {
LanguageModelError,
PortAutoForwardAction,
PortAttributes,
DebugVisualization
DebugVisualization,
TerminalShellExecutionCommandLineConfidence
} from './types-impl';
import { AuthenticationExtImpl } from './authentication-ext';
import { SymbolKind } from '../common/plugin-api-rpc-model';
Expand Down Expand Up @@ -636,6 +637,12 @@ export function createAPIFactory(

/** @stubbed ShareProvider */
registerShareProvider: () => Disposable.NULL,
/** @stubbed Terminal Shell Ingration */
onDidChangeTerminalShellIntegration: Event.None,
/** @stubbed Terminal Shell Ingration */
onDidEndTerminalShellExecution: Event.None,
/** @stubbed Terminal Shell Ingration */
onDidStartTerminalShellExecution: Event.None
};

const workspace: typeof theia.workspace = {
Expand Down Expand Up @@ -1494,7 +1501,8 @@ export function createAPIFactory(
LanguageModelError,
PortAutoForwardAction,
PortAttributes,
DebugVisualization
DebugVisualization,
TerminalShellExecutionCommandLineConfidence
};
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/plugin/terminal-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ export class TerminalExtImpl implements theia.Terminal {
this.creationOptions = this.options;
}

shellIntegration: theia.TerminalShellIntegration | undefined = undefined;

sendText(text: string, shouldExecute: boolean = true): void {
this.id.promise.then(id => this.proxy.$sendText(id, text, shouldExecute));
}
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3999,3 +3999,13 @@ export class DebugVisualization {
}

// #endregion

// #region Terminal Shell Integration

export enum TerminalShellExecutionCommandLineConfidence {
Low = 0,
Medium = 1,
High = 2
}

// #endregion
1 change: 1 addition & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import './theia.proposed.resolvers';
import './theia.proposed.scmValidation';
import './theia.proposed.shareProvider';
import './theia.proposed.terminalQuickFixProvider';
import './theia.proposed.terminalShellIntegration';
import './theia.proposed.textSearchProvider';
import './theia.proposed.timeline';

Expand Down
329 changes: 329 additions & 0 deletions packages/plugin/src/theia.proposed.terminalShellIntegration.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
// *****************************************************************************
// Copyright (C) 2024 Typefox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

declare module '@theia/plugin' {

// https://github.com/microsoft/vscode/issues/145234

/**
* A command that was executed in a terminal.
*/
export interface TerminalShellExecution {
/**
* The command line that was executed. The {@link TerminalShellExecutionCommandLineConfidence confidence}
* of this value depends on the specific shell's shell integration implementation. This
* value may become more accurate after {@link window.onDidEndTerminalShellExecution} is
* fired.
*
* @example
* // Log the details of the command line on start and end
* window.onDidStartTerminalShellExecution(event => {
* const commandLine = event.execution.commandLine;
* console.log(`Command started\n${summarizeCommandLine(commandLine)}`);
* });
* window.onDidEndTerminalShellExecution(event => {
* const commandLine = event.execution.commandLine;
* console.log(`Command ended\n${summarizeCommandLine(commandLine)}`);
* });
* function summarizeCommandLine(commandLine: TerminalShellExecutionCommandLine) {
* return [
* ` Command line: ${command.ommandLine.value}`,
* ` Confidence: ${command.ommandLine.confidence}`,
* ` Trusted: ${command.ommandLine.isTrusted}
* ].join('\n');
* }
*/
readonly commandLine: TerminalShellExecutionCommandLine;

/**
* The working directory that was reported by the shell when this command executed. This
* {@link Uri} may represent a file on another machine (eg. ssh into another machine). This
* requires the shell integration to support working directory reporting.
*/
readonly cwd: Uri | undefined;

/**
* Creates a stream of raw data (including escape sequences) that is written to the
* terminal. This will only include data that was written after `read` was called for
* the first time, ie. you must call `read` immediately after the command is executed via
* {@link TerminalShellIntegration.executeCommand} or
* {@link window.onDidStartTerminalShellExecution} to not miss any data.
*
* @example
* // Log all data written to the terminal for a command
* const command = term.shellIntegration.executeCommand({ commandLine: 'echo "Hello world"' });
* const stream = command.read();
* for await (const data of stream) {
* console.log(data);
* }
*/
read(): AsyncIterable<string>;
}

/**
* A command line that was executed in a terminal.
*/
export interface TerminalShellExecutionCommandLine {
/**
* The full command line that was executed, including both the command and its arguments.
*/
readonly value: string;

/**
* Whether the command line value came from a trusted source and is therefore safe to
* execute without user additional confirmation, such as a notification that asks "Do you
* want to execute (command)?". This verification is likely only needed if you are going to
* execute the command again.
*
* This is `true` only when the command line was reported explicitly by the shell
* integration script (ie. {@link TerminalShellExecutionCommandLineConfidence.High high confidence})
* and it used a nonce for verification.
*/
readonly isTrusted: boolean;

/**
* The confidence of the command line value which is determined by how the value was
* obtained. This depends upon the implementation of the shell integration script.
*/
readonly confidence: TerminalShellExecutionCommandLineConfidence;
}

/**
* The confidence of a {@link TerminalShellExecutionCommandLine} value.
*/
enum TerminalShellExecutionCommandLineConfidence {
/**
* The command line value confidence is low. This means that the value was read from the
* terminal buffer using markers reported by the shell integration script. Additionally one
* of the following conditions will be met:
*
* - The command started on the very left-most column which is unusual, or
* - The command is multi-line which is more difficult to accurately detect due to line
* continuation characters and right prompts.
* - Command line markers were not reported by the shell integration script.
*/
Low = 0,

/**
* The command line value confidence is medium. This means that the value was read from the
* terminal buffer using markers reported by the shell integration script. The command is
* single-line and does not start on the very left-most column (which is unusual).
*/
Medium = 1,

/**
* The command line value confidence is high. This means that the value was explicitly sent
* from the shell integration script or the command was executed via the
* {@link TerminalShellIntegration.executeCommand} API.
*/
High = 2
}

export interface Terminal {
/**
* An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered
* features for the terminal. This will always be `undefined` immediately after the terminal
* is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified
* when shell integration is activated for a terminal.
*
* Note that this object may remain undefined if shell integation never activates. For
* example Command Prompt does not support shell integration and a user's shell setup could
* conflict with the automatic shell integration activation.
*/
readonly shellIntegration: TerminalShellIntegration | undefined;
}

/**
* [Shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered capabilities owned by a terminal.
*/
export interface TerminalShellIntegration {
/**
* The current working directory of the terminal. This {@link Uri} may represent a file on
* another machine (eg. ssh into another machine). This requires the shell integration to
* support working directory reporting.
*/
readonly cwd: Uri | undefined;

/**
* Execute a command, sending ^C as necessary to interrupt any running command if needed.
*
* @param commandLine The command line to execute, this is the exact text that will be sent
* to the terminal.
*
* @example
* // Execute a command in a terminal immediately after being created
* const myTerm = window.createTerminal();
* window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
* if (terminal === myTerm) {
* const command = shellIntegration.executeCommand('echo "Hello world"');
* const code = await command.exitCode;
* console.log(`Command exited with code ${code}`);
* }
* }));
* // Fallback to sendText if there is no shell integration within 3 seconds of launching
* setTimeout(() => {
* if (!myTerm.shellIntegration) {
* myTerm.sendText('echo "Hello world"');
* // Without shell integration, we can't know when the command has finished or what the
* // exit code was.
* }
* }, 3000);
*
* @example
* // Send command to terminal that has been alive for a while
* const commandLine = 'echo "Hello world"';
* if (term.shellIntegration) {
* const command = term.shellIntegration.executeCommand({ commandLine });
* const code = await command.exitCode;
* console.log(`Command exited with code ${code}`);
* } else {
* term.sendText(commandLine);
* // Without shell integration, we can't know when the command has finished or what the
* // exit code was.
* }
*/
executeCommand(commandLine: string): TerminalShellExecution;

/**
* Execute a command, sending ^C as necessary to interrupt any running command if needed.
*
* *Note* This is not guaranteed to work as [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)
* must be activated. Check whether {@link TerminalShellExecution.exitCode} is rejected to
* verify whether it was successful.
*
* @param command A command to run.
* @param args Arguments to launch the executable with which will be automatically escaped
* based on the executable type.
*
* @example
* // Execute a command in a terminal immediately after being created
* const myTerm = window.createTerminal();
* window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => {
* if (terminal === myTerm) {
* const command = shellIntegration.executeCommand({
* command: 'echo',
* args: ['Hello world']
* });
* const code = await command.exitCode;
* console.log(`Command exited with code ${code}`);
* }
* }));
* // Fallback to sendText if there is no shell integration within 3 seconds of launching
* setTimeout(() => {
* if (!myTerm.shellIntegration) {
* myTerm.sendText('echo "Hello world"');
* // Without shell integration, we can't know when the command has finished or what the
* // exit code was.
* }
* }, 3000);
*
* @example
* // Send command to terminal that has been alive for a while
* const commandLine = 'echo "Hello world"';
* if (term.shellIntegration) {
* const command = term.shellIntegration.executeCommand({
* command: 'echo',
* args: ['Hello world']
* });
* const code = await command.exitCode;
* console.log(`Command exited with code ${code}`);
* } else {
* term.sendText(commandLine);
* // Without shell integration, we can't know when the command has finished or what the
* // exit code was.
* }
*/
executeCommand(executable: string, args: string[]): TerminalShellExecution;
}

export interface TerminalShellIntegrationChangeEvent {
/**
* The terminal that shell integration has been activated in.
*/
readonly terminal: Terminal;

/**
* The shell integration object.
*/
readonly shellIntegration: TerminalShellIntegration;
}

export interface TerminalShellExecutionStartEvent {
/**
* The terminal that shell integration has been activated in.
*/
readonly terminal: Terminal;

/**
* The shell integration object.
*/
readonly shellIntegration: TerminalShellIntegration;

/**
* The terminal shell execution that has ended.
*/
readonly execution: TerminalShellExecution;
}

export interface TerminalShellExecutionEndEvent {
/**
* The terminal that shell integration has been activated in.
*/
readonly terminal: Terminal;

/**
* The shell integration object.
*/
readonly shellIntegration: TerminalShellIntegration;

/**
* The terminal shell execution that has ended.
*/
readonly execution: TerminalShellExecution;

/**
* The exit code reported by the shell. `undefined` means the shell did not report an exit
* code or the shell reported a command started before the command finished.
*/
readonly exitCode: number | undefined;
}

export namespace window {
/**
* Fires when shell integration activates or one of its properties changes in a terminal.
*/
export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>;

/**
* This will be fired when a terminal command is started. This event will fire only when
* [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
* activated for the terminal.
*/
export const onDidStartTerminalShellExecution: Event<TerminalShellExecutionStartEvent>;

/**
* This will be fired when a terminal command is ended. This event will fire only when
* [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is
* activated for the terminal.
*/
export const onDidEndTerminalShellExecution: Event<TerminalShellExecutionEndEvent>;
}
}
Loading

0 comments on commit 5af9da6

Please sign in to comment.