UNPKG

sussudio

Version:

An unofficial VS Code Internal API

504 lines (503 loc) 22.5 kB
/*--------------------------------------------------------------------------------------------- * 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 }; }