Skip to content

Allow extensions to contribute links to the terminal (aka link providers) #91290

@Tyriar

Description

@Tyriar

Currently the way terminal links work is by waiting until the viewport has stopped updating and then parsing the entire viewport. This has a few problems, most notable the performance cost of validating this every time (particularly bad on some environments, terminal.integrated.enableFileLinks was added to allow people to disable it) and this awfulness:

const lineAndColumnClause = [
'((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
'((\\S*)",((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205]
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
'(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with []
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
].join('|').replace(/ /g, `[${'\u00A0'} ]`);

xterm.js just recently merged a new link provider API that allows moving away from this regex approach and instead resolving links when a hover happens. The PR to adopt this API is here #90336.

@connor4312 and I just wrote up this proposal as a starting point for allowing extensions to leverage this API. This has some really cool possibilities:

  • Language-specific handling, for example Node.js stack traces allowing to ctrl+click directly to node sources (eg. at Script.runInThisContext (vm.js:96:20))
  • Compiler-specific line/col formatting instead of core having to add each one explicitly
  • Auto-correcting CLIs based on suggestions
  • Handling of file links not relative to the initial cwd (this will probably be in core eventually)
  • Live share forwarding ports (instead of parsing all terminal output that it does now)
  interface LinkHoverContext {
    line: string;
    index: number;
    terminal: Terminal;
  }

  export interface TerminalLinkProvider {
    provideTerminalLink(context: LinkHoverContext): ProviderResult<TerminalLink>

    /**
     * Optionally provides custom handling logic for links returned from this
     * provider. This method can mutate the link `target`, or handle the
     * link internally. If a link is returned from this method, then VS
     * Code will try to open it using its default logic.
     */
    handleTerminalLink?(link: ResolvedTerminalLink): ProviderResult<ResolvedTerminalLink>;
  }

  interface TerminalLink {
    startIndex: number;
    length: number;

    /**
     * The uri this link points to. If set, and {@link TerminalLinkProvider.handlerTerminalLink}
     * is not implemented or returns false, then VS Code will try to open the Uri.
     */
    target?: Uri;

    /**
     * The tooltip text when you hover over this link.
     *
     * If a tooltip is provided, is will be displayed in a string that includes instructions on how to
     * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS,
     * user settings, and localization.
     */
    tooltip?: string;
  }

  interface ResolvedTerminalLink {
    /**
     * The link text from the terminal, derived from the startIndex and length
     * returned in {@link TerminalLink}.
     */
    text: string;

    /**
     * The uri this link points to. If set in the link returned from {@link TerminalLinkProvider.handlerTerminalLink},
     * then VS Code will try to open it.
     */
    target?: Uri;

    /**
     * The tooltip text when you hover over this link.
     *
     * If a tooltip is provided, is will be displayed in a string that includes instructions on how to
     * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS,
     * user settings, and localization.
     */
    tooltip?: string;
  }

Some notes/questions/unknowns:

  • How to handle conflicting links?
    • It looks like DocumentLinkProvider just overwrites links based on the most recently registered provider
    • Advise that implementers try match links as precisely/narrowly as possible
  • How would it work if Live Share used this API and js-debug did too?
    • Don't have an answer, the order being potentially inconsistent (based on activation time) is one problem. If it were consistent, js-debug could call into Live Share to get it to do the forwarding ahead of time, not ideal though.
  • Should this be generic to documents as well?
    • Since DocumentLinkProvider gets the links for the entire file this is slow for really large files and it does not allow validating links. An example of this is the GitHub issue links where #999999 would get a link that fails to open.
    • Files deal with ranges, the terminal doesn't
    • The downside of doing this is that we cannot show underlines until after hovering. The terminal never did this anyway as it would conflict with terminal underline attributes.

Metadata

Metadata

Assignees

Labels

apiapi-finalizationfeature-requestRequest for new features or functionalityinsiders-releasedPatch has been released in VS Code InsidersterminalGeneral terminal issues that don't fall under another labelverification-neededVerification of issue is requestedverifiedVerification succeeded

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions