sussudio
Version:
An unofficial VS Code Internal API
504 lines (503 loc) • 22.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { Disposable, dispose, toDisposable } from "../../../../base/common/lifecycle.mjs";
import { TerminalCapabilityStore } from "../capabilities/terminalCapabilityStore.mjs";
import { CommandDetectionCapability } from "../capabilities/commandDetectionCapability.mjs";
import { CwdDetectionCapability } from "../capabilities/cwdDetectionCapability.mjs";
import { PartialCommandDetectionCapability } from "../capabilities/partialCommandDetectionCapability.mjs";
import { ILogService } from "../../../log/common/log.mjs";
import { Emitter } from "../../../../base/common/event.mjs";
import { BufferMarkCapability } from "../capabilities/bufferMarkCapability.mjs";
import { URI } from "../../../../base/common/uri.mjs";
import { sanitizeCwd } from "../terminalEnvironment.mjs";
/**
* Shell integration is a feature that enhances the terminal's understanding of what's happening
* in the shell by injecting special sequences into the shell's prompt using the "Set Text
* Parameters" sequence (`OSC Ps ; Pt ST`).
*
* Definitions:
* - OSC: `\x1b]`
* - Ps: A single (usually optional) numeric parameter, composed of one or more digits.
* - Pt: A text parameter composed of printable characters.
* - ST: `\x7`
*
* This is inspired by a feature of the same name in the FinalTerm, iTerm2 and kitty terminals.
*/
/**
* The identifier for the first numeric parameter (`Ps`) for OSC commands used by shell integration.
*/
var ShellIntegrationOscPs;
(function (ShellIntegrationOscPs) {
/**
* Sequences pioneered by FinalTerm.
*/
ShellIntegrationOscPs[ShellIntegrationOscPs["FinalTerm"] = 133] = "FinalTerm";
/**
* Sequences pioneered by VS Code. The number is derived from the least significant digit of
* "VSC" when encoded in hex ("VSC" = 0x56, 0x53, 0x43).
*/
ShellIntegrationOscPs[ShellIntegrationOscPs["VSCode"] = 633] = "VSCode";
/**
* Sequences pioneered by iTerm.
*/
ShellIntegrationOscPs[ShellIntegrationOscPs["ITerm"] = 1337] = "ITerm";
ShellIntegrationOscPs[ShellIntegrationOscPs["SetCwd"] = 7] = "SetCwd";
ShellIntegrationOscPs[ShellIntegrationOscPs["SetWindowsFriendlyCwd"] = 9] = "SetWindowsFriendlyCwd";
})(ShellIntegrationOscPs || (ShellIntegrationOscPs = {}));
/**
* VS Code-specific shell integration sequences. Some of these are based on more common alternatives
* like those pioneered in FinalTerm. The decision to move to entirely custom sequences was to try
* to improve reliability and prevent the possibility of applications confusing the terminal. If
* multiple shell integration scripts run, VS Code will prioritize the VS Code-specific ones.
*
* It's recommended that authors of shell integration scripts use the common sequences (eg. 133)
* when building general purpose scripts and the VS Code-specific (633) when targeting only VS Code
* or when there are no other alternatives.
*/
var VSCodeOscPt;
(function (VSCodeOscPt) {
/**
* The start of the prompt, this is expected to always appear at the start of a line.
* Based on FinalTerm's `OSC 133 ; A ST`.
*/
VSCodeOscPt["PromptStart"] = "A";
/**
* The start of a command, ie. where the user inputs their command.
* Based on FinalTerm's `OSC 133 ; B ST`.
*/
VSCodeOscPt["CommandStart"] = "B";
/**
* Sent just before the command output begins.
* Based on FinalTerm's `OSC 133 ; C ST`.
*/
VSCodeOscPt["CommandExecuted"] = "C";
/**
* Sent just after a command has finished. The exit code is optional, when not specified it
* means no command was run (ie. enter on empty prompt or ctrl+c).
* Based on FinalTerm's `OSC 133 ; D [; <ExitCode>] ST`.
*/
VSCodeOscPt["CommandFinished"] = "D";
/**
* Explicitly set the command line. This helps workaround performance and reliability problems
* with parsing out the command, such as conpty not guaranteeing the position of the sequence or
* the shell not guaranteeing that the entire command is even visible.
*
* The command line can escape ascii characters using the `\xAB` format, where AB are the
* hexadecimal representation of the character code (case insensitive), and escape the `\`
* character using `\\`. It's required to escape semi-colon (`0x3b`) and characters 0x20 and
* below, this is particularly important for new line and semi-colon.
*
* Some examples:
*
* ```
* "\" -> "\\"
* "\n" -> "\x0a"
* ";" -> "\x3b"
* ```
*/
VSCodeOscPt["CommandLine"] = "E";
/**
* Similar to prompt start but for line continuations.
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
VSCodeOscPt["ContinuationStart"] = "F";
/**
* Similar to command start but for line continuations.
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
VSCodeOscPt["ContinuationEnd"] = "G";
/**
* The start of the right prompt.
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
VSCodeOscPt["RightPromptStart"] = "H";
/**
* The end of the right prompt.
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
VSCodeOscPt["RightPromptEnd"] = "I";
/**
* Set an arbitrary property: `OSC 633 ; P ; <Property>=<Value> ST`, only known properties will
* be handled.
*
* Known properties:
*
* - `Cwd` - Reports the current working directory to the terminal.
* - `IsWindows` - Indicates whether the terminal is using a Windows backend like winpty or
* conpty. This may be used to enable additional heuristics as the positioning of the shell
* integration sequences are not guaranteed to be correct. Valid values: `True`, `False`.
*
* WARNING: Any other properties may be changed and are not guaranteed to work in the future.
*/
VSCodeOscPt["Property"] = "P";
/**
* Sets a mark/point-of-interest in the buffer. `OSC 633 ; SetMark [; Id=<string>] [; Hidden]`
* `Id` - The identifier of the mark that can be used to reference it
* `Hidden` - When set, the mark will be available to reference internally but will not visible
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
VSCodeOscPt["SetMark"] = "SetMark";
})(VSCodeOscPt || (VSCodeOscPt = {}));
/**
* ITerm sequences
*/
var ITermOscPt;
(function (ITermOscPt) {
/**
* Sets a mark/point-of-interest in the buffer. `OSC 1337 ; SetMark`
*/
ITermOscPt["SetMark"] = "SetMark";
/**
* Reports current working directory (CWD). `OSC 1337 ; CurrentDir=<Cwd> ST`
*/
ITermOscPt["CurrentDir"] = "CurrentDir";
})(ITermOscPt || (ITermOscPt = {}));
/**
* The shell integration addon extends xterm by reading shell integration sequences and creating
* capabilities and passing along relevant sequences to the capabilities. This is meant to
* encapsulate all handling/parsing of sequences so the capabilities don't need to.
*/
let ShellIntegrationAddon = class ShellIntegrationAddon extends Disposable {
_disableTelemetry;
_telemetryService;
_logService;
_terminal;
capabilities = new TerminalCapabilityStore();
_hasUpdatedTelemetry = false;
_activationTimeout;
_commonProtocolDisposables = [];
_status = 0 /* ShellIntegrationStatus.Off */;
get status() { return this._status; }
_onDidChangeStatus = new Emitter();
onDidChangeStatus = this._onDidChangeStatus.event;
constructor(_disableTelemetry, _telemetryService, _logService) {
super();
this._disableTelemetry = _disableTelemetry;
this._telemetryService = _telemetryService;
this._logService = _logService;
this._register(toDisposable(() => {
this._clearActivationTimeout();
this._disposeCommonProtocol();
}));
}
_disposeCommonProtocol() {
dispose(this._commonProtocolDisposables);
this._commonProtocolDisposables.length = 0;
}
activate(xterm) {
this._terminal = xterm;
this.capabilities.add(3 /* TerminalCapability.PartialCommandDetection */, new PartialCommandDetectionCapability(this._terminal));
this._register(xterm.parser.registerOscHandler(633 /* ShellIntegrationOscPs.VSCode */, data => this._handleVSCodeSequence(data)));
this._register(xterm.parser.registerOscHandler(1337 /* ShellIntegrationOscPs.ITerm */, data => this._doHandleITermSequence(data)));
this._commonProtocolDisposables.push(xterm.parser.registerOscHandler(133 /* ShellIntegrationOscPs.FinalTerm */, data => this._handleFinalTermSequence(data)));
this._register(xterm.parser.registerOscHandler(7 /* ShellIntegrationOscPs.SetCwd */, data => this._doHandleSetCwd(data)));
this._register(xterm.parser.registerOscHandler(9 /* ShellIntegrationOscPs.SetWindowsFriendlyCwd */, data => this._doHandleSetWindowsFriendlyCwd(data)));
this._ensureCapabilitiesOrAddFailureTelemetry();
}
_handleFinalTermSequence(data) {
const didHandle = this._doHandleFinalTermSequence(data);
if (this._status === 0 /* ShellIntegrationStatus.Off */) {
this._status = 1 /* ShellIntegrationStatus.FinalTerm */;
this._onDidChangeStatus.fire(this._status);
}
return didHandle;
}
_doHandleFinalTermSequence(data) {
if (!this._terminal) {
return false;
}
// Pass the sequence along to the capability
// It was considered to disable the common protocol in order to not confuse the VS Code
// shell integration if both happen for some reason. This doesn't work for powerlevel10k
// when instant prompt is enabled though. If this does end up being a problem we could pass
// a type flag through the capability calls
const [command, ...args] = data.split(';');
switch (command) {
case 'A':
this._createOrGetCommandDetection(this._terminal).handlePromptStart();
return true;
case 'B':
// Ignore the command line for these sequences as it's unreliable for example in powerlevel10k
this._createOrGetCommandDetection(this._terminal).handleCommandStart({ ignoreCommandLine: true });
return true;
case 'C':
this._createOrGetCommandDetection(this._terminal).handleCommandExecuted();
return true;
case 'D': {
const exitCode = args.length === 1 ? parseInt(args[0]) : undefined;
this._createOrGetCommandDetection(this._terminal).handleCommandFinished(exitCode);
return true;
}
}
return false;
}
_handleVSCodeSequence(data) {
const didHandle = this._doHandleVSCodeSequence(data);
if (!this._hasUpdatedTelemetry && didHandle) {
this._telemetryService?.publicLog2('terminal/shellIntegrationActivationSucceeded');
this._hasUpdatedTelemetry = true;
this._clearActivationTimeout();
}
if (this._status !== 2 /* ShellIntegrationStatus.VSCode */) {
this._status = 2 /* ShellIntegrationStatus.VSCode */;
this._onDidChangeStatus.fire(this._status);
}
return didHandle;
}
async _ensureCapabilitiesOrAddFailureTelemetry() {
if (!this._telemetryService || this._disableTelemetry) {
return;
}
this._activationTimeout = setTimeout(() => {
if (!this.capabilities.get(2 /* TerminalCapability.CommandDetection */) && !this.capabilities.get(0 /* TerminalCapability.CwdDetection */)) {
this._telemetryService?.publicLog2('terminal/shellIntegrationActivationTimeout');
this._logService.warn('Shell integration failed to add capabilities within 10 seconds');
}
this._hasUpdatedTelemetry = true;
}, 10000);
}
_clearActivationTimeout() {
if (this._activationTimeout !== undefined) {
clearTimeout(this._activationTimeout);
this._activationTimeout = undefined;
}
}
_doHandleVSCodeSequence(data) {
if (!this._terminal) {
return false;
}
// Pass the sequence along to the capability
const [command, ...args] = data.split(';');
switch (command) {
case "A" /* VSCodeOscPt.PromptStart */:
this._createOrGetCommandDetection(this._terminal).handlePromptStart();
return true;
case "B" /* VSCodeOscPt.CommandStart */:
this._createOrGetCommandDetection(this._terminal).handleCommandStart();
return true;
case "C" /* VSCodeOscPt.CommandExecuted */:
this._createOrGetCommandDetection(this._terminal).handleCommandExecuted();
return true;
case "D" /* VSCodeOscPt.CommandFinished */: {
const exitCode = args.length === 1 ? parseInt(args[0]) : undefined;
this._createOrGetCommandDetection(this._terminal).handleCommandFinished(exitCode);
return true;
}
case "E" /* VSCodeOscPt.CommandLine */: {
let commandLine;
if (args.length === 1) {
commandLine = deserializeMessage(args[0]);
}
else {
commandLine = '';
}
this._createOrGetCommandDetection(this._terminal).setCommandLine(commandLine);
return true;
}
case "F" /* VSCodeOscPt.ContinuationStart */: {
this._createOrGetCommandDetection(this._terminal).handleContinuationStart();
return true;
}
case "G" /* VSCodeOscPt.ContinuationEnd */: {
this._createOrGetCommandDetection(this._terminal).handleContinuationEnd();
return true;
}
case "H" /* VSCodeOscPt.RightPromptStart */: {
this._createOrGetCommandDetection(this._terminal).handleRightPromptStart();
return true;
}
case "I" /* VSCodeOscPt.RightPromptEnd */: {
this._createOrGetCommandDetection(this._terminal).handleRightPromptEnd();
return true;
}
case "P" /* VSCodeOscPt.Property */: {
const deserialized = args.length ? deserializeMessage(args[0]) : '';
const { key, value } = parseKeyValueAssignment(deserialized);
if (value === undefined) {
return true;
}
switch (key) {
case 'Cwd': {
this._updateCwd(value);
return true;
}
case 'IsWindows': {
this._createOrGetCommandDetection(this._terminal).setIsWindowsPty(value === 'True' ? true : false);
return true;
}
case 'Task': {
this._createOrGetBufferMarkDetection(this._terminal);
this.capabilities.get(2 /* TerminalCapability.CommandDetection */)?.setIsCommandStorageDisabled();
return true;
}
}
}
case "SetMark" /* VSCodeOscPt.SetMark */: {
this._createOrGetBufferMarkDetection(this._terminal).addMark(parseMarkSequence(args));
return true;
}
}
// Unrecognized sequence
return false;
}
_updateCwd(value) {
value = sanitizeCwd(value);
this._createOrGetCwdDetection().updateCwd(value);
const commandDetection = this.capabilities.get(2 /* TerminalCapability.CommandDetection */);
commandDetection?.setCwd(value);
}
_doHandleITermSequence(data) {
if (!this._terminal) {
return false;
}
const [command] = data.split(';');
switch (command) {
case "SetMark" /* ITermOscPt.SetMark */: {
this._createOrGetBufferMarkDetection(this._terminal).addMark();
}
default: {
// Checking for known `<key>=<value>` pairs.
// Note that unlike `VSCodeOscPt.Property`, iTerm2 does not interpret backslash or hex-escape sequences.
// See: https://github.com/gnachman/iTerm2/blob/bb0882332cec5196e4de4a4225978d746e935279/sources/VT100Terminal.m#L2089-L2105
const { key, value } = parseKeyValueAssignment(command);
if (value === undefined) {
// No '=' was found, so it's not a property assignment.
return true;
}
switch (key) {
case "CurrentDir" /* ITermOscPt.CurrentDir */:
// Encountered: `OSC 1337 ; CurrentDir=<Cwd> ST`
this._updateCwd(value);
return true;
}
}
}
// Unrecognized sequence
return false;
}
_doHandleSetWindowsFriendlyCwd(data) {
if (!this._terminal) {
return false;
}
const [command, ...args] = data.split(';');
switch (command) {
case '9':
// Encountered `OSC 9 ; 9 ; <cwd> ST`
if (args.length) {
this._updateCwd(args[0]);
}
return true;
}
// Unrecognized sequence
return false;
}
/**
* Handles the sequence: `OSC 7 ; scheme://cwd ST`
*/
_doHandleSetCwd(data) {
if (!this._terminal) {
return false;
}
const [command] = data.split(';');
if (command.match(/^file:\/\/.*\//)) {
const uri = URI.parse(command);
if (uri.path && uri.path.length > 0) {
this._updateCwd(uri.path);
return true;
}
}
// Unrecognized sequence
return false;
}
serialize() {
if (!this._terminal || !this.capabilities.has(2 /* TerminalCapability.CommandDetection */)) {
return {
isWindowsPty: false,
commands: []
};
}
const result = this._createOrGetCommandDetection(this._terminal).serialize();
return result;
}
deserialize(serialized) {
if (!this._terminal) {
throw new Error('Cannot restore commands before addon is activated');
}
this._createOrGetCommandDetection(this._terminal).deserialize(serialized);
}
_createOrGetCwdDetection() {
let cwdDetection = this.capabilities.get(0 /* TerminalCapability.CwdDetection */);
if (!cwdDetection) {
cwdDetection = new CwdDetectionCapability();
this.capabilities.add(0 /* TerminalCapability.CwdDetection */, cwdDetection);
}
return cwdDetection;
}
_createOrGetCommandDetection(terminal) {
let commandDetection = this.capabilities.get(2 /* TerminalCapability.CommandDetection */);
if (!commandDetection) {
commandDetection = new CommandDetectionCapability(terminal, this._logService);
this.capabilities.add(2 /* TerminalCapability.CommandDetection */, commandDetection);
}
return commandDetection;
}
_createOrGetBufferMarkDetection(terminal) {
let bufferMarkDetection = this.capabilities.get(4 /* TerminalCapability.BufferMarkDetection */);
if (!bufferMarkDetection) {
bufferMarkDetection = new BufferMarkCapability(terminal);
this.capabilities.add(4 /* TerminalCapability.BufferMarkDetection */, bufferMarkDetection);
}
return bufferMarkDetection;
}
};
ShellIntegrationAddon = __decorate([
__param(2, ILogService)
], ShellIntegrationAddon);
export { ShellIntegrationAddon };
export function deserializeMessage(message) {
return message.replaceAll(
// Backslash ('\') followed by an escape operator: either another '\', or 'x' and two hex chars.
/\\(\\|x([0-9a-f]{2}))/gi,
// If it's a hex value, parse it to a character.
// Otherwise the operator is '\', which we return literally, now unescaped.
(_match, op, hex) => hex ? String.fromCharCode(parseInt(hex, 16)) : op);
}
export function parseKeyValueAssignment(message) {
const separatorIndex = message.indexOf('=');
if (separatorIndex === -1) {
return { key: message, value: undefined }; // No '=' was found.
}
return {
key: message.substring(0, separatorIndex),
value: message.substring(1 + separatorIndex)
};
}
export function parseMarkSequence(sequence) {
let id = undefined;
let hidden = false;
for (const property of sequence) {
if (property === 'Hidden') {
hidden = true;
}
if (property.startsWith('Id=')) {
id = property.substring(3);
}
}
return { id, hidden };
}