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

Windows: Polish single prompt line and clear/cls behavior #198463

Merged
merged 6 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 5 additions & 5 deletions src/vs/platform/terminal/common/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,12 @@ interface IBaseTerminalCommand {

export interface ITerminalCommand extends IBaseTerminalCommand {
// Optional non-serializable
promptStartMarker?: IMarker;
marker?: IXtermMarker;
readonly promptStartMarker?: IMarker;
readonly marker?: IXtermMarker;
endMarker?: IXtermMarker;
executedMarker?: IXtermMarker;
aliases?: string[][];
wasReplayed?: boolean;
readonly executedMarker?: IXtermMarker;
readonly aliases?: string[][];
readonly wasReplayed?: boolean;

getOutput(): string | undefined;
getOutputMatch(outputMatcher: ITerminalOutputMatcher): ITerminalOutputMatch | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class TerminalCommand implements ITerminalCommand {
get promptStartMarker() { return this._properties.promptStartMarker; }
get marker() { return this._properties.marker; }
get endMarker() { return this._properties.endMarker; }
set endMarker(value: IXtermMarker | undefined) { this._properties.endMarker = value; }
get executedMarker() { return this._properties.executedMarker; }
get aliases() { return this._properties.aliases; }
get wasReplayed() { return this._properties.wasReplayed; }
Expand Down Expand Up @@ -199,8 +200,6 @@ export class TerminalCommand implements ITerminalCommand {
}

export interface ICurrentPartialCommand {
previousCommandMarker?: IMarker;

promptStartMarker?: IMarker;

commandStartMarker?: IMarker;
Expand Down Expand Up @@ -238,8 +237,6 @@ export interface ICurrentPartialCommand {
}

export class PartialTerminalCommand implements ICurrentPartialCommand {
previousCommandMarker?: IMarker;

promptStartMarker?: IMarker;

commandStartMarker?: IMarker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { debounce } from 'vs/base/common/decorators';
import { Emitter } from 'vs/base/common/event';
import { Disposable, MandatoryMutableDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { CommandInvalidationReason, ICommandDetectionCapability, ICommandInvalidationRequest, IHandleCommandOptions, ISerializedCommandDetectionCapability, ISerializedTerminalCommand, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { CommandInvalidationReason, ICommandDetectionCapability, ICommandInvalidationRequest, IHandleCommandOptions, ISerializedCommandDetectionCapability, ISerializedTerminalCommand, ITerminalCommand, IXtermMarker, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ITerminalOutputMatcher } from 'vs/platform/terminal/common/terminal';

// Importing types is safe in any layer
Expand All @@ -32,6 +32,9 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
private __isCommandStorageDisabled: boolean = false;
private _handleCommandStartOptions?: IHandleCommandOptions;


private _commitCommandFinished?: RunOnceScheduler;

private _ptyHeuristicsHooks: ICommandDetectionHeuristicsHooks;
private _ptyHeuristics: MandatoryMutableDisposable<IPtyHeuristics>;

Expand Down Expand Up @@ -168,7 +171,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
get isCommandStorageDisabled() { return that.__isCommandStorageDisabled; }
get commandMarkers() { return that._commandMarkers; }
set commandMarkers(value) { that._commandMarkers = value; }
get clearCommandsInViewport() { return that._clearCommandsInViewport; }
get clearCommandsInViewport() { return that._clearCommandsInViewport.bind(that); }
},
this._logService
);
Expand Down Expand Up @@ -200,7 +203,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe

// Iterate backwards through commands to find the right one
for (let i = this.commands.length - 1; i >= 0; i--) {
if ((this.commands[i].promptStartMarker ?? this.commands[i].marker!).line <= line - 1) {
if ((this.commands[i].promptStartMarker ?? this.commands[i].marker!).line <= line) {
return this.commands[i];
}
}
Expand All @@ -224,7 +227,19 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
}

handlePromptStart(options?: IHandleCommandOptions): void {
this._currentCommand.promptStartMarker = options?.marker || this._terminal.registerMarker(0);
// Adjust the last command's finished marker when needed. The standard position for the
// finished marker `D` to appear is at the same position as the following prompt started
// `A`.
const lastCommand = this.commands.at(-1);
if (lastCommand?.endMarker && lastCommand?.executedMarker && lastCommand.endMarker.line === lastCommand.executedMarker.line) {
this._logService.debug('CommandDetectionCapability#handlePromptStart adjusted commandFinished', `${lastCommand.endMarker.line} -> ${lastCommand.executedMarker.line + 1}`);
lastCommand.endMarker = cloneMarker(this._terminal, lastCommand.executedMarker, 1);
}
this._commitCommandFinished?.flush();
this._commitCommandFinished = undefined;

this._currentCommand.promptStartMarker = options?.marker || (lastCommand?.endMarker ? cloneMarker(this._terminal, lastCommand.endMarker) : this._terminal.registerMarker(0));

this._logService.debug('CommandDetectionCapability#handlePromptStart', this._terminal.buffer.active.cursorX, this._currentCommand.promptStartMarker?.line);
}

Expand Down Expand Up @@ -286,7 +301,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
}

handleCommandFinished(exitCode: number | undefined, options?: IHandleCommandOptions): void {
this._ptyHeuristics.value?.preHandleCommandFinished?.();
this._ptyHeuristics.value.preHandleCommandFinished?.();

this._logService.debug('CommandDetectionCapability#handleCommandFinished', this._terminal.buffer.active.cursorX, options?.marker?.line, this._currentCommand.command, this._currentCommand);

Expand All @@ -306,21 +321,23 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
return;
}

this._ptyHeuristics.value?.postHandleCommandFinished?.();

this._currentCommand.commandFinishedMarker = options?.marker || this._terminal.registerMarker(0);

this._ptyHeuristics.value.postHandleCommandFinished?.();

const newCommand = this._currentCommand.promoteToFullCommand(this._cwd, exitCode, this._handleCommandStartOptions?.ignoreCommandLine ?? false, options?.markProperties);

if (newCommand) {
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);

this._onBeforeCommandFinished.fire(newCommand);
if (!this._currentCommand.isInvalid) {
this._onCommandFinished.fire(newCommand);
}
this._commitCommandFinished = new RunOnceScheduler(() => {
this._onBeforeCommandFinished.fire(newCommand);
if (!this._currentCommand.isInvalid) {
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
this._onCommandFinished.fire(newCommand);
}
}, 50);
this._commitCommandFinished.schedule();
}
this._currentCommand.previousCommandMarker = this._currentCommand.commandStartMarker;
this._currentCommand = new PartialTerminalCommand(this._terminal);
this._handleCommandStartOptions = undefined;
}
Expand Down Expand Up @@ -383,8 +400,8 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
*/
interface ICommandDetectionHeuristicsHooks {
readonly onCurrentCommandInvalidatedEmitter: Emitter<ICommandInvalidationRequest>;
readonly onCommandExecutedEmitter: Emitter<void>;
readonly onCommandStartedEmitter: Emitter<ITerminalCommand>;
readonly onCommandExecutedEmitter: Emitter<void>;
readonly dimensions: ITerminalDimensions;
readonly isCommandStorageDisabled: boolean;

Expand Down Expand Up @@ -487,7 +504,8 @@ const enum AdjustCommandStartMarkerConstants {
class WindowsPtyHeuristics extends Disposable {

private _onCursorMoveListener = this._register(new MutableDisposable());
private _windowsPromptPollingInProcess: boolean = false;

private _recentlyPerformedCsiJ = false;

constructor(
private readonly _terminal: Terminal,
Expand All @@ -497,11 +515,27 @@ class WindowsPtyHeuristics extends Disposable {
) {
super();

this._register(_terminal.parser.registerCsiHandler({ final: 'J' }, params => {
if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) {
this._recentlyPerformedCsiJ = true;
this._hooks.clearCommandsInViewport();
}
// We don't want to override xterm.js' default behavior, just augment it
return false;
}));

this._register(this._capability.onBeforeCommandFinished(command => {
// For a Windows backend we cannot listen to CSI J, instead we assume running clear or
// cls will clear all commands in the viewport. This is not perfect but it's right most
// of the time.
if (this._recentlyPerformedCsiJ) {
this._recentlyPerformedCsiJ = false;
return;
}

// For older Windows backends we cannot listen to CSI J, instead we assume running clear
// or cls will clear all commands in the viewport. This is not perfect but it's right
// most of the time.
if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') {
this._tryAdjustCommandStartMarkerScheduler?.cancel();
this._tryAdjustCommandStartMarkerScheduler = undefined;
this._hooks.clearCommandsInViewport();
this._capability.currentCommand.isInvalid = true;
this._hooks.onCurrentCommandInvalidatedEmitter.fire({ reason: CommandInvalidationReason.Windows });
Expand Down Expand Up @@ -563,16 +597,16 @@ class WindowsPtyHeuristics extends Disposable {
private _tryAdjustCommandStartMarkerPollCount: number = 0;

async handleCommandStart() {

if (this._windowsPromptPollingInProcess) {
this._windowsPromptPollingInProcess = false;
}
this._capability.currentCommand.commandStartX = this._terminal.buffer.active.cursorX;

// On Windows track all cursor movements after the command start sequence
this._hooks.commandMarkers.length = 0;

const initialCommandStartMarker = this._capability.currentCommand.commandStartMarker = this._terminal.registerMarker(0)!;
const initialCommandStartMarker = this._capability.currentCommand.commandStartMarker = (
this._capability.currentCommand.promptStartMarker
? cloneMarker(this._terminal, this._capability.currentCommand.promptStartMarker)
: this._terminal.registerMarker(0)
)!;
this._capability.currentCommand.commandStartX = 0;

// DEBUG: Add a decoration for the original unadjusted command start position
Expand Down Expand Up @@ -621,10 +655,16 @@ class WindowsPtyHeuristics extends Disposable {
if (this._cursorOnNextLine()) {
const prompt = this._getWindowsPrompt(start.line + scannedLineCount);
if (prompt) {
const adjustedPrompt = typeof prompt === 'string' ? prompt : prompt.prompt;
this._capability.currentCommand.commandStartMarker = this._terminal.registerMarker(0)!;
if (typeof prompt === 'object' && prompt.likelySingleLine) {
this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted promptStart', `${this._capability.currentCommand.promptStartMarker?.line} -> ${this._capability.currentCommand.commandStartMarker.line}`);
this._capability.currentCommand.promptStartMarker?.dispose();
this._capability.currentCommand.promptStartMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker);
}
// use the regex to set the position as it's possible input has occurred
this._capability.currentCommand.commandStartX = prompt.length;
this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker successfully adjusted', `${start.line} -> ${this._capability.currentCommand.commandStartMarker}:${this._capability.currentCommand.commandStartX}`);
this._capability.currentCommand.commandStartX = adjustedPrompt.length;
this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted commandStart', `${start.line} -> ${this._capability.currentCommand.commandStartMarker.line}:${this._capability.currentCommand.commandStartX}`);
this._flushPendingHandleCommandStartTask();
return;
}
Expand Down Expand Up @@ -812,7 +852,7 @@ class WindowsPtyHeuristics extends Disposable {
});
}

private _getWindowsPrompt(y: number = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY): string | undefined {
private _getWindowsPrompt(y: number = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY): string | { prompt: string; likelySingleLine: true } | undefined {
const line = this._terminal.buffer.active.getLine(y);
if (!line) {
return;
Expand All @@ -828,7 +868,10 @@ class WindowsPtyHeuristics extends Disposable {
if (pwshPrompt) {
const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, '>');
if (adjustedPrompt) {
return adjustedPrompt;
return {
prompt: adjustedPrompt,
likelySingleLine: true
};
}
}

Expand All @@ -843,7 +886,10 @@ class WindowsPtyHeuristics extends Disposable {

// Command Prompt
const cmdMatch = lineText.match(/^(?<prompt>(\(.+\)\s)?(?:[A-Z]:\\.*>))/);
return cmdMatch?.groups?.prompt;
return cmdMatch?.groups?.prompt ? {
prompt: cmdMatch.groups.prompt,
likelySingleLine: true
} : undefined;
}

private _adjustPrompt(prompt: string | undefined, lineText: string, char: string): string | undefined {
Expand Down Expand Up @@ -918,3 +964,7 @@ function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number
}
return content;
}

function cloneMarker(xterm: Terminal, marker: IXtermMarker, offset: number = 0): IXtermMarker | undefined {
return xterm.registerMarker(marker.line - (xterm.buffer.active.baseY + xterm.buffer.active.cursorY) + offset);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ suite('CommandDetectionCapability', () => {
capability.handleCommandFinished(exitCode);
}

async function printCommandStart(prompt: string) {
capability.handlePromptStart();
await writeP(xterm, `\r${prompt}`);
}


setup(async () => {
disposables = new DisposableStore();
const TerminalCtor = (await importAMDNodeModule<typeof import('@xterm/xterm')>('@xterm/xterm', 'lib/xterm.js')).Terminal;
Expand All @@ -86,6 +92,7 @@ suite('CommandDetectionCapability', () => {

test('should add commands for expected capability method calls', async () => {
await printStandardCommand('$ ', 'echo foo', 'foo', undefined, 0);
await printCommandStart('$ ');
assertCommands([{
command: 'echo foo',
exitCode: 0,
Expand All @@ -96,6 +103,7 @@ suite('CommandDetectionCapability', () => {

test('should trim the command when command executed appears on the following line', async () => {
await printStandardCommand('$ ', 'echo foo\r\n', 'foo', undefined, 0);
await printCommandStart('$ ');
assertCommands([{
command: 'echo foo',
exitCode: 0,
Expand All @@ -108,6 +116,7 @@ suite('CommandDetectionCapability', () => {
test('should add cwd to commands when it\'s set', async () => {
await printStandardCommand('$ ', 'echo foo', 'foo', '/home', 0);
await printStandardCommand('$ ', 'echo bar', 'bar', '/home/second', 0);
await printCommandStart('$ ');
assertCommands([
{ command: 'echo foo', exitCode: 0, cwd: '/home', marker: { line: 0 } },
{ command: 'echo bar', exitCode: 0, cwd: '/home/second', marker: { line: 2 } }
Expand All @@ -116,6 +125,7 @@ suite('CommandDetectionCapability', () => {
test('should add old cwd to commands if no cwd sequence is output', async () => {
await printStandardCommand('$ ', 'echo foo', 'foo', '/home', 0);
await printStandardCommand('$ ', 'echo bar', 'bar', undefined, 0);
await printCommandStart('$ ');
assertCommands([
{ command: 'echo foo', exitCode: 0, cwd: '/home', marker: { line: 0 } },
{ command: 'echo bar', exitCode: 0, cwd: '/home', marker: { line: 2 } }
Expand All @@ -124,6 +134,7 @@ suite('CommandDetectionCapability', () => {
test('should use an undefined cwd if it\'s not set initially', async () => {
await printStandardCommand('$ ', 'echo foo', 'foo', undefined, 0);
await printStandardCommand('$ ', 'echo bar', 'bar', '/home', 0);
await printCommandStart('$ ');
assertCommands([
{ command: 'echo foo', exitCode: 0, cwd: undefined, marker: { line: 0 } },
{ command: 'echo bar', exitCode: 0, cwd: '/home', marker: { line: 2 } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@
.monaco-workbench .xterm.dev-mode .xterm-sequence-decoration.bottom.right {
transform: scale(.5) translate(50%, 50%);
}
.monaco-workbench .xterm.dev-mode .xterm-sequence-decoration.color-0 {
color: #FF4444;
}
.monaco-workbench .xterm.dev-mode .xterm-sequence-decoration.color-1 {
color: #44FFFF;
}
Loading
Loading