diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 9de9d8dcc6a7f..edac3d7b2b7dc 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -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'; @@ -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 = { @@ -1494,7 +1501,8 @@ export function createAPIFactory( LanguageModelError, PortAutoForwardAction, PortAttributes, - DebugVisualization + DebugVisualization, + TerminalShellExecutionCommandLineConfidence }; }; } diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index d8e2a3f01bd70..3e450bc88a329 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -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)); } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index e1b1dd10bb3a7..1aee501f2e810 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -3999,3 +3999,13 @@ export class DebugVisualization { } // #endregion + +// #region Terminal Shell Integration + +export enum TerminalShellExecutionCommandLineConfidence { + Low = 0, + Medium = 1, + High = 2 +} + +// #endregion diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 7134d063da377..4ce0f20f46c2a 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -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'; diff --git a/packages/plugin/src/theia.proposed.terminalShellIntegration.d.ts b/packages/plugin/src/theia.proposed.terminalShellIntegration.d.ts new file mode 100644 index 0000000000000..e9f945fb87e20 --- /dev/null +++ b/packages/plugin/src/theia.proposed.terminalShellIntegration.d.ts @@ -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; + } + + /** + * 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; + + /** + * 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; + + /** + * 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; + } +} diff --git a/packages/vsx-registry/src/node/vsx-extension-resolver.ts b/packages/vsx-registry/src/node/vsx-extension-resolver.ts index 01f6b5f2173d3..5f21d3cb53df0 100644 --- a/packages/vsx-registry/src/node/vsx-extension-resolver.ts +++ b/packages/vsx-registry/src/node/vsx-extension-resolver.ts @@ -54,7 +54,7 @@ export class VSXExtensionResolver implements PluginDeployerResolver { const filter = await this.vsxApiFilter(); const version = options?.version || id.version; if (version) { - console.log(`[${id}]: trying to resolve version ${version}...`); + console.log(`[${id.id}]: trying to resolve version ${version}...`); extension = await filter.findLatestCompatibleExtension({ extensionId: id.id, extensionVersion: version, @@ -62,7 +62,7 @@ export class VSXExtensionResolver implements PluginDeployerResolver { targetPlatform: VSXExtensionResolver.TARGET_PLATFORM }); } else { - console.log(`[${id}]: trying to resolve latest version...`); + console.log(`[${id.id}]: trying to resolve latest version...`); extension = await filter.findLatestCompatibleExtension({ extensionId: id.id, includeAllVersions: true, @@ -82,7 +82,7 @@ export class VSXExtensionResolver implements PluginDeployerResolver { if (!options?.ignoreOtherVersions) { const existingVersion = this.hasSameOrNewerVersion(id.id, extension); if (existingVersion) { - console.log(`[${id}]: is already installed with the same or newer version '${existingVersion}'`); + console.log(`[${id.id}]: is already installed with the same or newer version '${existingVersion}'`); return; } }