microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
164.24k stars 29.3k forks source link

Expose shell integration command knowledge to extensions #145234

Closed Tyriar closed 3 months ago

Tyriar commented 2 years ago

Shell integration in the terminal could enable APIs like being able to listen to commands that run and get their output/exit code.

Rough example:

interface Terminal {
  executeCommand(command): Thenable<TerminalCommandResult>;
}
ClockworkV commented 2 years ago

I came here to request this, and here it is already. Being in Backlog means it'll get on the work plan eventually?

Cheers, looking forward to this, it's the missing link for my 'debug last command line' extension!

Tyriar commented 2 years ago

@ClockworkV backlog means it's a valid item and it may be worked on in the future. I'm pretty sure this one will happen within the next year

meganrogge commented 2 years ago

Proposal:

registerQuickFixProvider(...options: ITerminalQuickFixOptions[]): void;

export interface ITerminalQuickFixOptions {
    commandLineMatcher: string | RegExp;
    outputMatcher?: ITerminalOutputMatcher;
    getQuickFixes: TerminalQuickFixCallback;
    exitStatus?: boolean;
}
export type TerminalQuickFixMatchResult = { commandLineMatch: RegExpMatchArray; outputMatch?: RegExpMatchArray | null };
export type TerminalQuickFixAction = IAction | ITerminalQuickFixCommandAction | ITerminalQuickFixOpenerAction;
export type TerminalQuickFixCallback = (matchResult: TerminalQuickFixMatchResult, command: ITerminalCommand) => TerminalQuickFixAction[] | TerminalQuickFixAction | undefined;

export interface ITerminalQuickFixCommandAction {
    type: 'command';
    command: string;
    addNewLine: boolean;
}
export interface ITerminalQuickFixOpenerAction {
    type: 'opener';
    uri: URI;
}
export interface ITerminalOutputMatcher {
    /**
     * A string or regex to match against the unwrapped line. If this is a regex with the multiline
     * flag, it will scan an amount of lines equal to `\n` instances in the regex + 1.
     */
    lineMatcher: string | RegExp;
    /**
     * Which side of the output to anchor the {@link offset} and {@link length} against.
     */
    anchor: 'top' | 'bottom';
    /**
     * How far from either the top or the bottom of the butter to start matching against.
     */
    offset: number;
    /**
     * The number of rows to match against, this should be as small as possible for performance
     * reasons.
     */
    length: number;
}

@alexr00 and @lszomoru how does this look to you, as you'll be adopting this for the git extension?

Tyriar commented 2 years ago

@meganrogge I've forked this off to https://github.com/microsoft/vscode/issues/162950 as I realized this issue is more about general listening to commands

mmisiewicz commented 1 year ago

Voting for this issue. The proposed interface would exactly solve a problem I have developing an extension.

lroberts7 commented 1 year ago

+1 for this, can we get it on a roadmap plz?

mmisiewicz-yext commented 1 year ago

Hi @meganrogge and @Tyriar - I've noticed this issue has been on an "iteration plan" in Nov and Oct of 2022. I'm wondering if you think it will be picked up any time soon? Thanks, really appreciate all the hard work!

Tyriar commented 1 year ago

@mmisiewicz I'll actually likely be looking into a proposed API for this in July. Maybe not an executeCommand API but at least the ability to extract command+output after the fact

Tyriar commented 1 year ago

Here's an early proposal:

interface TerminalExecuteCommandEvent {
    terminal: Terminal;
    command: TerminalCommand;
}

interface TerminalCommand {
    commandLine: string;
    // string because we may not know how to convert the cwd reported via shell
    // integration to a URI
    cwd: Uri | string | undefined;
    result: Thenable<TerminalExecuteCommandResult>;
}

interface TerminalExecuteCommandResult {
    exitCode: number;
    output: string;
}

namespace window {
    export const onDidExecuteTerminalCommand: Event<TerminalExecuteCommandEvent>;

    export interface Terminal {
        // Throws when {@link TerminalState.supportsCommandExecution} is false?
        executeCommand(command: string): Thenable<TerminalCommand>;
    }

    export interface TerminalState {
        // Reading this too early would say false, should this start as undefined?
        supportsCommandExecution: boolean;
    }
}

It doesn't look like this will make it in the July release though.

mmisiewicz commented 1 year ago

Hi @Tyriar this looks great! It would definitely solve my needs (I only need exit code, not the command output). Would love to use this in August 🤞

andyleejordan commented 1 year ago

Wow I hate to be person to ask this, but what are some scenarios for this, and will all commands executed result in the event being fired? I'm trying to reason about how this would fit with the PowerShell Extension (where we have our own Extension Terminal, that does support shell integration by virtue of copying the same setup script into shell for the ASCII control codes).

First thing that comes to mind is that maybe this would replace our use of workbench.action.terminal.runSelectedText (which is what we call for PowerShell.RunSelection, i.e. select some PowerShell code and run it in the active terminal). Would the command still show up in the active terminal as if it were pasted, or would it run "in the background?"

Tyriar commented 1 year ago

@andyleejordan provided shell integration is enabled, it would be a more reliable way of running a command as we have logic that will trigger ^C if VS Code is unsure if there's anything in the prompt (always on Windows but improvements are planned).

The biggest thing this gives that people have been after is exit code and output reporting, both of which are impossible currently.

Tyriar commented 1 year ago

Needs an early discussion in the API sync before we start impl.

Tyriar commented 1 year ago

Feedback:

Tyriar commented 1 year ago

Feedback:

Tyriar commented 1 year ago

Latest:

declare module 'vscode' {

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

    export interface TerminalExecutedCommand {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;
        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;
        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;
        /**
         * The exit code reported by the shell.
         */
        exitCode: number | undefined;
        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         */
        output: string | undefined;
    }

    export namespace window {
        /**
         * An event that is emitted when a terminal with shell integration activated has completed
         * executing a command.
         *
         * Note that this event will not fire if the executed command exits the shell, listen to
         * {@link onDidCloseTerminal} to handle that case.
         */
        export const onDidExecuteTerminalCommand: Event<TerminalExecutedCommand>;
    }
}
starball5 commented 1 year ago

Related on Stack Overflow:

Also vscode snippet variables: how to get content from output of command line program

karrtikr commented 11 months ago

@Tyriar Does this (should we) also account for cases where we can stream through as output gets printed?

JustinGrote commented 11 months ago

@karrtikr seems reasonable to me to expose this either as a stream or as an async generator. I can see some use cases such as real time output parsing for test adapters or log parsers.

FlorentP42 commented 11 months ago

Gathering interest for Shell Script auto-indent: https://github.com/microsoft/vscode/issues/199003

Tyriar commented 11 months ago

@FlorentP42 please do not link to random issues to try get more attention on it. It's just spam that causes notifications and wastes time.

Tyriar commented 11 months ago

@karrtikr @JustinGrote the terminalDataWriteEvent API is essentially the streaming API you're talking about and it has some major performance issues as all data needs to be passed to the extension host (due to API implementation guidelines). We can't really do streaming without sending over escape sequences too which I was explicitly trying to avoid with this one in order to make it more generally useful.

@JustinGrote I think for tests specifically we have special handling of that via test extensions that integrate with the testing view.

FlorentP42 commented 11 months ago

@Tyriar: If you have better (less intrusive) ways to get interest on a given ticket, could you please share how you would proceed?

Tyriar commented 11 months ago

@FlorentP42 social media or friends? Just you shouldn't post off topic stuff to our bug tracker as it's distracting.

FlorentP42 commented 11 months ago

@FlorentP42 social media or friends? Just you shouldn't post off topic stuff to our bug tracker as it's distracting.

I do not use any social media, nor do I have friends interested in such topics X) So I assumed people interested in similar VSCode issues would have the most chances to also be interested in the one I posted... Yet it does not sem to be the case here, so I am sorry for the inconvenience.

sinemakinci1 commented 8 months ago

Hi there,

Wanted to +1 this as it unlocks our ability to work on colorized build output which is the number 2 actively and top-voted issue for the CMake Tools Extension by giving the extension more knowledge of the terminal: https://github.com/microsoft/vscode-cmake-tools/issues/478. Is there any ability for the team to work this to unblock our users high requests for greater visibility of their build?

Tyriar commented 8 months ago

@sinemakinci1 can you elaborate on how this API would help you? Skimming the issue it seems like you would be after either allowing colors to be controlled in an output channel more finely (just textmate grammars atm?) or the existing Pseudoterminal extension API? This particular issue tracks the ability to be able to listen to when commands execute and get their output, it's nothing to do with adding colors to output?

sinemakinci1 commented 8 months ago

@Tyriar Secondary to getting ANSI color support in the terminal, our back up request would be that we want to atleast have more of the terminal exposed so that we can potentially build this support ourselves for the extension as a workaround.

Tyriar commented 8 months ago

@sinemakinci1 I'm still a little confused, the terminal supports ANSI escape sequences for color and always has, the output view does not as it's an editor behind the scenes and colors via textmate grammars.

This issue is specifically about an API for the terminal. If you do indeed run a process in a terminal, ANSI escape sequences will be passed through and work, you can write manually via a Pseudoterminal and you can intercept input/output via this same API if you need to for some reason.

JustinGrote commented 8 months ago

Can confirm that we do this with Pester Tests extension for our runs, I think @sinemakinci1 is conflating shell integration knowledge with ANSI which are just output characters.

gcampbell-msft commented 8 months ago

@Tyriar @JustinGrote To fill in some context for @sinemakinci1's questions.

We have two separate asks that would separately help us solve some problems for ensuring that users who run build can get colorized output, either from the output channel or from the terminal.

  1. Supporting ANSI and colorization in the output channel. This, based on your response, seems like it is not supporting any further than textmate grammers. Thanks for this context. We could likely either suggest users to install extensions that already do this, or provide our own.

  2. Support the ask from this issue, the ability to execute a command and get its result. This will help us be able to get result information, both for whether the build succeeded or failed, as well as simply knowing when the build completed. More context on this can also be found here: https://github.com/microsoft/vscode-cmake-tools/issues/2783. We currently can't directly integrate with sending our build to the terminal, we only support it through users creating a tasks.json entry, because we'd need this API in order to get result information from the command.

Does this provide more understanding and context? Thanks!

Tyriar commented 8 months ago

@gcampbell-msft I'm not too familiar with the tasks extension API but could you contribute a shell task to get the colors working there?

If you want to know when users run a cmake command in any terminal then this request makes sense, bringing colors into the discussion is the main thing that was confusing me 🙂

gcampbell-msft commented 8 months ago

@Tyriar Yes, I believe that has been our strategy in the past, we have a setting that users can enable that will use tasks.json build tasks so that they can get colorization. However, it looks like at some point this broke for us (we can investigate this), and so we're hoping that this API can be enabled so that we can, instead of relying on keeping a tasks.json entry up-to-date and correct, we can directly invoke a build (or any command) with the extension and automatically use the terminal so that users can get colorization with no effort of their own.

Tyriar commented 8 months ago

However, it looks like at some point this broke for us (we can investigate this), and so we're hoping that this API can be enabled so that we can, instead of relying on keeping a tasks.json entry up-to-date and correct

This is a little surprising as all a task does is run a command in the terminal, if the type is shell it will run it within a shell.

gcampbell-msft commented 8 months ago

@Tyriar Ah, it seems like it's because we actually attempt to provide a cmake task type, and when users use this, colorization seems not to be there. However, if they switch to a shell task type, colorization is there.

This could be possibly a bug within our own task type, that we should investigate, or, could it be a bug with VS Code where technically we should still get colorization with this?

Tyriar commented 8 months ago

@gcampbell-msft I'm guessing for a cmake task type you would need to wrap the call in a shell yourself something like bash -c "cmake arg1 arg2"? I'm not sure if you're able to do this via the API so we handle the shell stuff (eg. using the default shell, cmd on Windows, etc.). @meganrogge might have more info?

starball5 commented 8 months ago

Is this task to run CMake configuration? or to run build? At least for build, check https://stackoverflow.com/q/9963783/11107541 and see if that makes any difference. Do you have a link to an issue ticket about this I can look at?

meganrogge commented 8 months ago

However, it looks like at some point this broke for us (we can investigate this)

what broke for you?

gcampbell-msft commented 8 months ago

@starball5 The task is to run CMake build: https://github.com/microsoft/vscode-cmake-tools/issues/2783#issuecomment-1841819111.

@meganrogge It may not be that it broke, but that the way we are doing tasks right now removes the colors, I think this may be because we are doing our own cmake task type. Here is the relevant issue comment: https://github.com/microsoft/vscode-cmake-tools/issues/2783#issuecomment-1273581199. Thanks.

milestones95 commented 8 months ago

I have an extension using the onDidWriteTerminalData proposed API. However it is retrieving the output from the terminal of my extension window. I want to retrieve the output of application's terminal (the extension host) that is running my extension. Is this possible? The purpose of this is so that my extension can help debug the extension host's code by viewing the console.log statements.

Tyriar commented 8 months ago

@milestones95 onDidWriteTerminalData accesses the data send to vscode terminals, console.logs are not put in a vscode terminal but the devtools console. You'd need to use a different approach.

Tyriar commented 8 months ago

I'm planning on iterating more on this in March. Also considering the idea of stabilizing onDidWriteTerminalData again, instead of worrying about extensions using and disposing of the listeners for performance reasons, perhaps I could flip it around and prevent misuse by rate limiting the event for very busy terminals.

whitequark commented 8 months ago

Also considering the idea of stabilizing onDidWriteTerminalData again, instead of worrying about extensions using and disposing of the listeners for performance reasons, perhaps I could flip it around and prevent misuse by rate limiting the event for very busy terminals.

Could this event be per-terminal rather than per-window?

Tyriar commented 8 months ago

@whitequark that's the easy fix, but unfortunately (fortunately?) a hardline approach is taken to extension API guidelines and since Terminal objects are public objects, the event must be global https://github.com/microsoft/vscode/wiki/Extension-API-guidelines

whitequark commented 8 months ago

That is a little cursed but hard to argue with. A solution could be to batch the events so that no more than x events per terminal per second are emitted.

Tyriar commented 8 months ago

Current state of these proposed APIs:

declare module 'vscode' {

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

    export interface TerminalExecutedCommand {
        /**
         * The {@link Terminal} the command was executed in.
         */
        terminal: Terminal;
        /**
         * The full command line that was executed, including both the command and the arguments.
         */
        commandLine: string | undefined;
        /**
         * The current working directory that was reported by the shell. This will be a {@link Uri}
         * if the string reported by the shell can reliably be mapped to the connected machine.
         */
        cwd: Uri | string | undefined;
        /**
         * The exit code reported by the shell.
         */
        exitCode: number | undefined;
        /**
         * The output of the command when it has finished executing. This is the plain text shown in
         * the terminal buffer and does not include raw escape sequences. Depending on the shell
         * setup, this may include the command line as part of the output.
         */
        output: string | undefined;
    }

    export namespace window {
        /**
         * An event that is emitted when a terminal with shell integration activated has completed
         * executing a command.
         *
         * Note that this event will not fire if the executed command exits the shell, listen to
         * {@link onDidCloseTerminal} to handle that case.
         */
        export const onDidExecuteTerminalCommand: Event<TerminalExecutedCommand>;
    }
}
declare module 'vscode' {

    // https://github.com/microsoft/vscode/issues/78502
    //
    // This API is still proposed but we don't intent on promoting it to stable due to problems
    // around performance. See #145234 for a more likely API to get stabilized.

    export interface TerminalDataWriteEvent {
        /**
         * The {@link Terminal} for which the data was written.
         */
        readonly terminal: Terminal;
        /**
         * The data being written.
         */
        readonly data: string;
    }

    namespace window {
        /**
         * An event which fires when the terminal's child pseudo-device is written to (the shell).
         * In other words, this provides access to the raw data stream from the process running
         * within the terminal, including VT sequences.
         */
        export const onDidWriteTerminalData: Event<TerminalDataWriteEvent>;
    }
}
mmisiewicz commented 8 months ago

This would be perfect for my use case, running a command and getting the result!

Tyriar commented 8 months ago

@mmisiewicz you can test that first one by enabling proposed APIs, you just can't publish it. The second one onDidWriteTerminalData likely won't become stable, however I think we need to add some way of streaming progress from the first.

whitequark commented 8 months ago

This would not work for my use case at all, where I would like to detect a string (an URL with a port number) written by a launched command and connect to that port while the command is active. This is something I need for a custom (non-DAP) debugger for a non-software use case.

Tyriar commented 8 months ago

@whitequark an event/stream to get the data of a command while it's happening would work though right?