UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

394 lines (393 loc) 14 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 '@sussudio/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 '@sussudio/base/common/event.mjs'; import { BufferMarkCapability } from '../capabilities/bufferMarkCapability.mjs'; import { URI } from '@sussudio/base/common/uri.mjs'; import { sanitizeCwd } from '../terminalEnvironment.mjs'; /** * 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 }; }