Skip to content

Commit

Permalink
Merge pull request #157783 from babakks/support-other-terminals-cwd-s…
Browse files Browse the repository at this point in the history
…equence

🎁 Support other terminals CWD escape sequence
  • Loading branch information
Tyriar committed Aug 19, 2022
2 parents 3400ec4 + 0d950ab commit 3cb5824
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 25 deletions.
126 changes: 103 additions & 23 deletions src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type { ITerminalAddon, Terminal } from 'xterm-headless';
import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';


/**
* Shell integration is a feature that enhances the terminal's understanding of what's happening
Expand Down Expand Up @@ -48,7 +50,9 @@ const enum ShellIntegrationOscPs {
/**
* Sequences pioneered by iTerm.
*/
ITerm = 1337
ITerm = 1337,
SetCwd = 7,
SetWindowsFriendlyCwd = 9
}

/**
Expand Down Expand Up @@ -158,7 +162,12 @@ const enum ITermOscPt {
/**
* Sets a mark/point-of-interest in the buffer. `OSC 1337 ; SetMark`
*/
SetMark = 'SetMark'
SetMark = 'SetMark',

/**
* Reports current working directory (CWD). `OSC 1337 ; CurrentDir=<Cwd> ST`
*/
CurrentDir = 'CurrentDir'
}

/**
Expand Down Expand Up @@ -204,6 +213,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
this._commonProtocolDisposables.push(
xterm.parser.registerOscHandler(ShellIntegrationOscPs.FinalTerm, data => this._handleFinalTermSequence(data))
);
this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.SetCwd, data => this._doHandleSetCwd(data)));
this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.SetWindowsFriendlyCwd, data => this._doHandleSetWindowsFriendlyCwd(data)));
this._ensureCapabilitiesOrAddFailureTelemetry();
}

Expand Down Expand Up @@ -306,7 +317,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
case VSCodeOscPt.CommandLine: {
let commandLine: string;
if (args.length === 1) {
commandLine = this._deserializeMessage(args[0]);
commandLine = deserializeMessage(args[0]);
} else {
commandLine = '';
}
Expand All @@ -330,20 +341,13 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
return true;
}
case VSCodeOscPt.Property: {
const [key, rawValue] = args[0].split('=');
if (rawValue === undefined) {
const { key, value } = parseKeyValueAssignment(args[0]);
if (value === undefined) {
return true;
}
const value = this._deserializeMessage(rawValue);
switch (key) {
case 'Cwd': {
// TODO: Ideally we would also support the following to supplement our own:
// - OSC 1337 ; CurrentDir=<Cwd> ST (iTerm)
// - OSC 7 ; scheme://cwd ST (Unknown origin)
// - OSC 9 ; 9 ; <cwd> ST (cmder)
this._createOrGetCwdDetection().updateCwd(value);
const commandDetection = this.capabilities.get(TerminalCapability.CommandDetection);
commandDetection?.setCwd(value);
this._updateCwd(value);
return true;
}
case 'IsWindows': {
Expand All @@ -361,6 +365,12 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
return false;
}

private _updateCwd(value: string) {
this._createOrGetCwdDetection().updateCwd(value);
const commandDetection = this.capabilities.get(TerminalCapability.CommandDetection);
commandDetection?.setCwd(value);
}

private _doHandleITermSequence(data: string): boolean {
if (!this._terminal) {
return false;
Expand All @@ -371,7 +381,65 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
case ITermOscPt.SetMark: {
this._createOrGetCommandDetection(this._terminal).handleGenericCommand({ genericMarkProperties: { disableCommandStorage: true } });
}
default: {
// Checking for known `<key>=<value>` pairs.
const { key, value } = parseKeyValueAssignment(command);

if (value === undefined) {
// No '=' was found, so it's not a property assignment.
return true;
}

switch (key) {
case ITermOscPt.CurrentDir:
// Encountered: `OSC 1337 ; CurrentDir=<Cwd> ST`
this._updateCwd(value);
return true;
}
}
}

// Unrecognized sequence
return false;
}

private _doHandleSetWindowsFriendlyCwd(data: string): boolean {
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`
*/
private _doHandleSetCwd(data: string): boolean {
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;
}
Expand Down Expand Up @@ -411,17 +479,29 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
}
return commandDetection;
}
}

private _deserializeMessage(message: string): string {
let result = message.replace(/\\\\/g, '\\');
const deserializeRegex = /\\x([0-9a-f]{2})/i;
while (true) {
const match = result.match(deserializeRegex);
if (!match?.index || match.length < 2) {
break;
}
result = result.slice(0, match.index) + String.fromCharCode(parseInt(match[1], 16)) + result.slice(match.index + 4);
export function deserializeMessage(message: string): string {
let result = message.replace(/\\\\/g, '\\');
const deserializeRegex = /\\x([0-9a-f]{2})/i;
while (true) {
const match = result.match(deserializeRegex);
if (!match?.index || match.length < 2) {
break;
}
return result;
result = result.slice(0, match.index) + String.fromCharCode(parseInt(match[1], 16)) + result.slice(match.index + 4);
}
return result;
}

export function parseKeyValueAssignment(message: string): { key: string; value: string | undefined } {
const deserialized = deserializeMessage(message);
const separatorIndex = deserialized.indexOf('=');
if (separatorIndex === -1) {
return { key: deserialized, value: undefined }; // No '=' was found.
}
return {
key: deserialized.substring(0, separatorIndex),
value: deserialized.substring(1 + separatorIndex)
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/

import { Terminal } from 'xterm';
import { strictEqual } from 'assert';
import { strictEqual, deepStrictEqual } from 'assert';
import { timeout } from 'vs/base/common/async';
import * as sinon from 'sinon';
import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';
import { parseKeyValueAssignment, ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
Expand Down Expand Up @@ -58,12 +58,90 @@ suite('ShellIntegrationAddon', () => {
await writeP(xterm, '\x1b]633;P;Cwd=/foo\x07');
strictEqual(capabilities.has(TerminalCapability.CwdDetection), true);
});

test('should pass cwd sequence to the capability', async () => {
const mock = shellIntegrationAddon.getCwdDectionMock();
mock.expects('updateCwd').once().withExactArgs('/foo');
await writeP(xterm, '\x1b]633;P;Cwd=/foo\x07');
mock.verify();
});

test('detect ITerm sequence: `OSC 1337 ; CurrentDir=<Cwd> ST`', async () => {
type TestCase = [title: string, input: string, expected: string];
const cases: TestCase[] = [
['root', '/', '/'],
['non-root', '/some/path', '/some/path'],
];
for (const x of cases) {
const [title, input, expected] = x;
const mock = shellIntegrationAddon.getCwdDectionMock();
mock.expects('updateCwd').once().withExactArgs(expected).named(title);
await writeP(xterm, `\x1b]1337;CurrentDir=${input}\x07`);
mock.verify();
}
});

suite('detect `SetCwd` sequence: `OSC 7; scheme://cwd ST`', async () => {
test('should accept well-formatted URLs', async () => {
type TestCase = [title: string, input: string, expected: string];
const cases: TestCase[] = [
// Different hostname values:
['empty hostname, pointing root', 'file:///', '/'],
['empty hostname', 'file:///test-root/local', '/test-root/local'],
['non-empty hostname', 'file://some-hostname/test-root/local', '/test-root/local'],
// URL-encoded chars:
['URL-encoded value (1)', 'file:///test-root/%6c%6f%63%61%6c', '/test-root/local'],
['URL-encoded value (2)', 'file:///test-root/local%22', '/test-root/local"'],
['URL-encoded value (3)', 'file:///test-root/local"', '/test-root/local"'],
];
for (const x of cases) {
const [title, input, expected] = x;
const mock = shellIntegrationAddon.getCwdDectionMock();
mock.expects('updateCwd').once().withExactArgs(expected).named(title);
await writeP(xterm, `\x1b]7;${input}\x07`);
mock.verify();
}
});

test('should ignore ill-formatted URLs', async () => {
type TestCase = [title: string, input: string];
const cases: TestCase[] = [
// Different hostname values:
['no hostname, pointing root', 'file://'],
// Non-`file` scheme values:
['no scheme (1)', '/test-root'],
['no scheme (2)', '//test-root'],
['no scheme (3)', '///test-root'],
['no scheme (4)', ':///test-root'],
['http', 'http:///test-root'],
['ftp', 'ftp:///test-root'],
['ssh', 'ssh:///test-root'],
];

for (const x of cases) {
const [title, input] = x;
const mock = shellIntegrationAddon.getCwdDectionMock();
mock.expects('updateCwd').never().named(title);
await writeP(xterm, `\x1b]7;${input}\x07`);
mock.verify();
}
});
});

test('detect `SetWindowsFrindlyCwd` sequence: `OSC 9 ; 9 ; <cwd> ST`', async () => {
type TestCase = [title: string, input: string, expected: string];
const cases: TestCase[] = [
['root', '/', '/'],
['non-root', '/some/path', '/some/path'],
];
for (const x of cases) {
const [title, input, expected] = x;
const mock = shellIntegrationAddon.getCwdDectionMock();
mock.expects('updateCwd').once().withExactArgs(expected).named(title);
await writeP(xterm, `\x1b]9;9;${input}\x07`);
mock.verify();
}
});
});

suite('command tracking', async () => {
Expand Down Expand Up @@ -134,3 +212,23 @@ suite('ShellIntegrationAddon', () => {
});
});
});

test('parseKeyValueAssignment', () => {
type TestCase = [title: string, input: string, expected: [key: string, value: string | undefined]];
const cases: TestCase[] = [
['empty', '', ['', undefined]],
['no "=" sign', 'some-text', ['some-text', undefined]],
['empty value', 'key=', ['key', '']],
['empty key', '=value', ['', 'value']],
['normal', 'key=value', ['key', 'value']],
['multiple "=" signs (1)', 'key==value', ['key', '=value']],
['multiple "=" signs (2)', 'key=value===true', ['key', 'value===true']],
['just a "="', '=', ['', '']],
['just a "=="', '==', ['', '=']],
];

cases.forEach(x => {
const [title, input, [key, value]] = x;
deepStrictEqual(parseKeyValueAssignment(input), { key, value }, title);
});
});

0 comments on commit 3cb5824

Please sign in to comment.