Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shell integration foundation #140196

Merged
merged 33 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
973363e
add shell integration addon
meganrogge Jan 5, 2022
c50372f
get CommandCognisant to work
meganrogge Jan 5, 2022
c27bbbe
bunch of errors
meganrogge Jan 5, 2022
f364112
Merge branch 'main' into merogge/shell
meganrogge Jan 5, 2022
601a1d1
fix one more
meganrogge Jan 5, 2022
a2b3155
hook it together
meganrogge Jan 6, 2022
e4e1eb7
get it to work
meganrogge Jan 6, 2022
8cd17ba
make time/date less verbose
meganrogge Jan 6, 2022
8c25da8
fix timeline
meganrogge Jan 6, 2022
1b48d76
add abstract command tracker
meganrogge Jan 6, 2022
58c7af5
on restore, restart shell integration
meganrogge Jan 6, 2022
8b423f0
use time from common
meganrogge Jan 6, 2022
01112a1
fix errors
meganrogge Jan 6, 2022
5efae82
add ago
meganrogge Jan 6, 2022
e647837
fix ago label the right way
meganrogge Jan 6, 2022
768bf94
merge
meganrogge Jan 6, 2022
eb18795
fix merge issue
meganrogge Jan 6, 2022
a96e9fd
use marker instead of listening to onData to track command value
meganrogge Jan 6, 2022
969b065
clean up
meganrogge Jan 6, 2022
f5bdf05
Pull shell integration browser-side logic into an addon
Tyriar Jan 6, 2022
6c8a870
remove eslint override
meganrogge Jan 7, 2022
f16b9b5
if not terminal, return
meganrogge Jan 7, 2022
c8ab575
improve handling of cwds
meganrogge Jan 7, 2022
2ecbdc8
Trim optional line endings correctly
Tyriar Jan 7, 2022
c1101ed
Get basic Windows shell integration mostly working
Tyriar Jan 7, 2022
29e30c7
Prefer Date.now over Date.getTime for simplicity/perf
Tyriar Jan 7, 2022
58cfe97
Treat -1 exit codes specially for pwsh
Tyriar Jan 7, 2022
068bf3e
Simplify frequency increment
Tyriar Jan 7, 2022
0e8224b
Don't pass capabilities to XtermTerminal
Tyriar Jan 7, 2022
728dadf
Remove shell integration plumbing between renderer and pty host
Tyriar Jan 7, 2022
a56b709
Use write, not writeln when enabling shell integration
Tyriar Jan 7, 2022
487b25d
Only re-write shell integration enable when reviving the buffer
Tyriar Jan 7, 2022
6985184
Remove unwanted event
Tyriar Jan 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@ export interface IHeartbeatService {
readonly onBeat: Event<void>;
}

export interface TerminalCommand {
command: string;
timestamp: number;
cwd?: string;
exitCode?: number;
}

export interface IShellLaunchConfig {
/**
* The name of the terminal, if this is not set the name of the process will be used.
Expand Down Expand Up @@ -548,7 +555,8 @@ export interface IProcessReadyEvent {
}

export const enum ProcessCapability {
CwdDetection = 'cwdDetection'
CwdDetection = 'cwdDetection',
CommandCognisant = 'commandCognisant'
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/vs/platform/terminal/node/ptyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ export class PersistentTerminalProcess extends Disposable {
class XtermSerializer implements ITerminalSerializer {
private _xterm: XtermTerminal;
private _unicodeAddon?: XtermUnicode11Addon;
private _isShellIntegrationEnabled: boolean = false;

constructor(
cols: number,
Expand All @@ -735,10 +736,23 @@ class XtermSerializer implements ITerminalSerializer {
this._xterm = new XtermTerminal({ cols, rows, scrollback });
if (reviveBuffer) {
this._xterm.writeln(reviveBuffer);
if (this._isShellIntegrationEnabled) {
this._xterm.write('\x1b033]133;E\x1b007');
}
}
this._xterm.parser.registerOscHandler(133, (data => this._handleShellIntegration(data)));
this.setUnicodeVersion(unicodeVersion);
}

private _handleShellIntegration(data: string): boolean {
const [command,] = data.split(';');
if (command === 'E') {
this._isShellIntegrationEnabled = true;
return true;
}
return false;
}

handleData(data: string): void {
this._xterm.write(data);
}
Expand Down
11 changes: 11 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,12 @@ export interface ITerminalInstance {
* clipboard.
*/
showLinkQuickpick(): Promise<void>;

/**
* Triggers a quick pick that displays recent commands or cwds. Selecting one will
* re-run it in the active terminal.
*/
runRecent(type: 'command' | 'cwd'): Promise<void>;
}

export interface IXtermTerminal {
Expand Down Expand Up @@ -839,6 +845,11 @@ export interface IXtermTerminal {
* viewport.
*/
clearBuffer(): void;

/*
* When process capabilites are updated, update the command tracker
*/
upgradeCommandTracker(): void;
}

export interface IRequestAddInstanceToGroupEvent {
Expand Down
28 changes: 28 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,34 @@ export function registerTerminalActions() {
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: TerminalCommandId.RunRecentCommand,
title: { value: localize('workbench.action.terminal.runRecentCommand', "Run Recent Command"), original: 'Run Recent Command' },
f1: true,
category,
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated)
});
}
async run(accessor: ServicesAccessor): Promise<void> {
await accessor.get(ITerminalService).activeInstance?.runRecent('command');
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: TerminalCommandId.GoToRecentDirectory,
title: { value: localize('workbench.action.terminal.goToRecentDirectory', "Go to Recent Directory"), original: 'Go to Recent Directory' },
f1: true,
category,
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated)
});
}
async run(accessor: ServicesAccessor): Promise<void> {
await accessor.get(ITerminalService).activeInstance?.runRecent('cwd');
}
});
registerAction2(class extends Action2 {
constructor() {
super({
Expand Down
47 changes: 47 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnviro
import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust';
import { isFirefox } from 'vs/base/browser/browser';
import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick';
import { fromNow } from 'vs/base/common/date';

const enum Constants {
/**
Expand Down Expand Up @@ -697,6 +698,52 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
return { wordLinks: wordResults, webLinks: webResults, fileLinks: fileResults };
}

async runRecent(type: 'command' | 'cwd'): Promise<void> {
const commands = this.xterm?.commandTracker.commands;
if (!commands || !this.xterm) {
return;
}
type Item = IQuickPickItem;
const items: Item[] = [];
if (type === 'command') {
for (const { command, timestamp, cwd, exitCode } of commands) {
// trim off any whitespace and/or line endings
const label = command.trim();
if (label.length === 0) {
continue;
}
let description = '';
if (cwd) {
description += `cwd: ${cwd} `;
}
if (exitCode) {
// Since you cannot get the last command's exit code on pwsh, just whether it failed
// or not, -1 is treated specially as simply failed
if (exitCode === -1) {
description += 'failed';
} else {
description += `exitCode: ${exitCode}`;
}
}
items.push({
label,
description: description.trim(),
detail: fromNow(timestamp, true),
id: timestamp.toString()
});
}
} else {
const cwds = this.xterm.commandTracker.cwds;
for (const label of cwds) {
items.push({ label });
}
}
const result = await this._quickInputService.pick(items.reverse(), {});
if (result) {
this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, true);
}
}

detachFromElement(): void {
this._wrapperElement?.remove();
this._container = undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { Terminal, IMarker } from 'xterm';
import { TerminalCommand } from 'vs/platform/terminal/common/terminal';
import { Emitter } from 'vs/base/common/event';
import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon';
import { ILogService } from 'vs/platform/log/common/log';
import { ShellIntegrationInfo, ShellIntegrationInteraction } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon';

interface ICurrentPartialCommand {
marker?: IMarker;
previousCommandMarker?: IMarker;
promptStartY?: number;
commandStartY?: number;
commandStartX?: number;
commandExecutedY?: number;
commandFinishedY?: number;
command?: string;
}

export class CognisantCommandTrackerAddon extends CommandTrackerAddon {
private _commands: TerminalCommand[] = [];
private _cwds = new Map<string, number>();
private _exitCode: number | undefined;
private _cwd: string | undefined;
private _currentCommand: ICurrentPartialCommand = {};

protected _terminal: Terminal | undefined;

private readonly _onCwdChanged = new Emitter<string>();
readonly onCwdChanged = this._onCwdChanged.event;

constructor(
@ILogService private readonly _logService: ILogService
) {
super();
}

activate(terminal: Terminal): void {
this._terminal = terminal;
}

handleIntegratedShellChange(event: { type: string, value: string }): void {
if (!this._terminal) {
return;
}
switch (event.type) {
case ShellIntegrationInfo.CurrentDir: {
this._cwd = event.value;
const freq = this._cwds.get(this._cwd) || 0;
this._cwds.set(this._cwd, freq + 1);
this._onCwdChanged.fire(this._cwd);
break;
}
case ShellIntegrationInteraction.PromptStart:
this._currentCommand.promptStartY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
break;
case ShellIntegrationInteraction.CommandStart:
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
this._currentCommand.commandStartY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;
this._currentCommand.marker = this._terminal.registerMarker(0);
break;
case ShellIntegrationInteraction.CommandExecuted:
this._currentCommand.commandExecutedY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY;

// TODO: Leverage key events on Windows between CommandStart and Executed to ensure we have the correct line

// TODO: Only do this on Windows backends
// Check if the command line is the same as the previous command line or if the
// start Y differs from the executed Y. This is to catch the conpty case where the
// "rendering" of the shell integration sequences doesn't occur on the correct cell
// due to https://github.com/microsoft/terminal/issues/11220
if (this._currentCommand.previousCommandMarker?.line === this._currentCommand.marker?.line ||
this._currentCommand.commandStartY === this._currentCommand.commandExecutedY) {
this._currentCommand.marker = this._terminal?.registerMarker(0);
this._currentCommand.commandStartX = 0;
}

// TODO: This does not yet work when the prompt line is wrapped
this._currentCommand.command = this._terminal!.buffer.active.getLine(this._currentCommand.commandExecutedY)?.translateToString(true, this._currentCommand.commandStartX || 0);

// TODO: Only do this on Windows backends
// Something went wrong, try predict the prompt based on the shell.
if (this._currentCommand.commandStartX === 0) {
// TODO: Only do this on pwsh
const promptPredictions = [
`PS ${this._cwd}> `,
`PS>`,
];
for (const promptPrediction of promptPredictions) {
if (this._currentCommand.command?.startsWith(promptPrediction)) {
// TODO: Consider cell vs string positioning; test CJK
this._currentCommand.commandStartX = promptPrediction.length;
this._currentCommand.command = this._currentCommand.command.substring(this._currentCommand.commandStartX);
break;
}
}
}
break;
case ShellIntegrationInteraction.CommandFinished:
this._logService.trace('Terminal Command Finished', this._currentCommand.command);
this._exitCode = Number.parseInt(event.value);
if (!this._currentCommand.marker?.line || !this._terminal.buffer.active) {
break;
}
if (this._currentCommand.command && !this._currentCommand.command.startsWith('\\') && this._currentCommand.command !== '') {
this._commands.push({
command: this._currentCommand.command,
timestamp: Date.now(),
cwd: this._cwd,
exitCode: this._exitCode
});
}

this._currentCommand.previousCommandMarker?.dispose();
this._currentCommand.previousCommandMarker = this._currentCommand.marker;
this._currentCommand.marker = undefined;
break;
default:
return;
}
}

get commands(): TerminalCommand[] {
return this._commands;
}

get cwds(): string[] {
const cwds = [];
const sorted = new Map([...this._cwds.entries()].sort((a, b) => b[1] - a[1]));
for (const [key,] of sorted.entries()) {
cwds.push(key);
}
return cwds;
}
}
Loading