@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
394 lines (393 loc) • 14 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 '@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 };
}