Skip to content

Commit

Permalink
enable adding and navigating to custom marks in the buffer (#158313)
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge authored Sep 7, 2022
1 parent aad3b4b commit 9e0db45
Show file tree
Hide file tree
Showing 24 changed files with 544 additions and 307 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Emitter } from 'vs/base/common/event';
import { IBufferMarkCapability, TerminalCapability, IMarkProperties } from 'vs/platform/terminal/common/capabilities/capabilities';
// Importing types is safe in any layer
// eslint-disable-next-line local/code-import-patterns
import type { IMarker, Terminal } from 'xterm-headless';

/**
* Manages "marks" in the buffer which are lines that are tracked when lines are added to or removed
* from the buffer.
*/
export class BufferMarkCapability implements IBufferMarkCapability {

readonly type = TerminalCapability.BufferMarkDetection;

private _idToMarkerMap: Map<string, IMarker> = new Map();
private _anonymousMarkers: IMarker[] = [];

private readonly _onMarkAdded = new Emitter<IMarkProperties>();
readonly onMarkAdded = this._onMarkAdded.event;

constructor(
private readonly _terminal: Terminal
) {
}

*markers(): IterableIterator<IMarker> {
for (const m of this._idToMarkerMap.values()) {
yield m;
}
for (const m of this._anonymousMarkers) {
yield m;
}
}

addMark(properties?: IMarkProperties): void {
const marker = properties?.marker || this._terminal.registerMarker();
const id = properties?.id;
if (!marker) {
return;
}
if (id) {
this._idToMarkerMap.set(id, marker);
marker.onDispose(() => this._idToMarkerMap.delete(id));
} else {
this._anonymousMarkers.push(marker);
marker.onDispose(() => this._anonymousMarkers.filter(m => m !== marker));
}
this._onMarkAdded.fire({ marker, id, hidden: properties?.hidden, hoverMessage: properties?.hoverMessage });
}

getMark(id: string): IMarker | undefined {
return this._idToMarkerMap.get(id);
}
}
56 changes: 50 additions & 6 deletions src/vs/platform/terminal/common/capabilities/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IGenericMarkProperties, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess';
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';

interface IEvent<T, U = void> {
(listener: (arg1: T, arg2: U) => any): IDisposable;
Expand Down Expand Up @@ -60,7 +60,14 @@ export const enum TerminalCapability {
* may not be so good at remembering the position of commands that ran in the past. This state
* may be enabled when something goes wrong or when using conpty for example.
*/
PartialCommandDetection
PartialCommandDetection,

/**
* Manages buffer marks that can be used for terminal navigation. The source of
* the request (task, debug, etc) provides an ID, optional marker, hoverMessage, and hidden property. When
* hidden is not provided, a generic decoration is added to the buffer and overview ruler.
*/
BufferMarkDetection
}

/**
Expand Down Expand Up @@ -103,6 +110,7 @@ export interface ITerminalCapabilityImplMap {
[TerminalCapability.CommandDetection]: ICommandDetectionCapability;
[TerminalCapability.NaiveCwdDetection]: INaiveCwdDetectionCapability;
[TerminalCapability.PartialCommandDetection]: IPartialCommandDetectionCapability;
[TerminalCapability.BufferMarkDetection]: IBufferMarkCapability;
}

export interface ICwdDetectionCapability {
Expand All @@ -122,6 +130,14 @@ export interface ICommandInvalidationRequest {
reason: CommandInvalidationReason;
}

export interface IBufferMarkCapability {
type: TerminalCapability.BufferMarkDetection;
markers(): IterableIterator<IMarker>;
onMarkAdded: Event<IMarkProperties>;
addMark(properties?: IMarkProperties): void;
getMark(id: string): IMarker | undefined;
}

export interface ICommandDetectionCapability {
readonly type: TerminalCapability.CommandDetection;
readonly commands: readonly ITerminalCommand[];
Expand All @@ -148,7 +164,6 @@ export interface ICommandDetectionCapability {
handleRightPromptStart(): void;
handleRightPromptEnd(): void;
handleCommandStart(options?: IHandleCommandOptions): void;
handleGenericCommand(options?: IHandleCommandOptions): void;
handleCommandExecuted(options?: IHandleCommandOptions): void;
handleCommandFinished(exitCode?: number, options?: IHandleCommandOptions): void;
invalidateCurrentCommand(request: ICommandInvalidationRequest): void;
Expand All @@ -170,10 +185,11 @@ export interface IHandleCommandOptions {
* The marker to use
*/
marker?: IMarker;

/**
* Properties for a generic mark
* Properties for the mark
*/
genericMarkProperties?: IGenericMarkProperties;
markProperties?: IMarkProperties;
}

export interface INaiveCwdDetectionCapability {
Expand All @@ -197,9 +213,9 @@ export interface ITerminalCommand {
endMarker?: IXtermMarker;
executedMarker?: IXtermMarker;
commandStartLineContent?: string;
markProperties?: IMarkProperties;
getOutput(): string | undefined;
hasOutput(): boolean;
genericMarkProperties?: IGenericMarkProperties;
}

/**
Expand All @@ -214,3 +230,31 @@ export interface IXtermMarker {
(listener: () => any): { dispose(): void };
};
}

export interface ISerializedCommand {
command: string;
cwd: string | undefined;
startLine: number | undefined;
startX: number | undefined;
endLine: number | undefined;
executedLine: number | undefined;
exitCode: number | undefined;
commandStartLineContent: string | undefined;
timestamp: number;
markProperties: IMarkProperties | undefined;
}
export interface IMarkProperties {
hoverMessage?: string;
disableCommandStorage?: boolean;
hidden?: boolean;
marker?: IMarker;
id?: string;
}
export interface ISerializedCommandDetectionCapability {
isWindowsPty: boolean;
commands: ISerializedCommand[];
}
export interface IPtyHostProcessReplayEvent {
events: ReplayEntry[];
commands: ISerializedCommandDetectionCapability;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { timeout } from 'vs/base/common/async';
import { debounce } from 'vs/base/common/decorators';
import { Emitter } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHandleCommandOptions, ICommandInvalidationRequest, CommandInvalidationReason } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess';
import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHandleCommandOptions, ICommandInvalidationRequest, CommandInvalidationReason, ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
// Importing types is safe in any layer
// eslint-disable-next-line local/code-import-patterns
import type { IBuffer, IBufferLine, IDisposable, IMarker, Terminal } from 'xterm-headless';
Expand Down Expand Up @@ -317,7 +316,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
}
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
this._currentCommand.commandStartMarker = options?.marker || this._terminal.registerMarker(0);
this._onCommandStarted.fire({ marker: options?.marker || this._currentCommand.commandStartMarker, genericMarkProperties: options?.genericMarkProperties } as ITerminalCommand);
this._onCommandStarted.fire({ marker: options?.marker || this._currentCommand.commandStartMarker, markProperties: options?.markProperties } as ITerminalCommand);
this._logService.debug('CommandDetectionCapability#handleCommandStart', this._currentCommand.commandStartX, this._currentCommand.commandStartMarker?.line);
}

Expand Down Expand Up @@ -353,7 +352,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
}

handleGenericCommand(options?: IHandleCommandOptions): void {
if (options?.genericMarkProperties?.disableCommandStorage) {
if (options?.markProperties?.disableCommandStorage) {
this.setIsCommandStorageDisabled();
}
this.handlePromptStart(options);
Expand Down Expand Up @@ -459,7 +458,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
commandStartLineContent: this._currentCommand.commandStartLineContent,
hasOutput: () => !executedMarker?.isDisposed && !endMarker?.isDisposed && !!(executedMarker && endMarker && executedMarker?.line < endMarker!.line),
getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer),
genericMarkProperties: options?.genericMarkProperties
markProperties: options?.markProperties
};
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
Expand Down Expand Up @@ -526,7 +525,8 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
cwd: e.cwd,
exitCode: e.exitCode,
commandStartLineContent: e.commandStartLineContent,
timestamp: e.timestamp
timestamp: e.timestamp,
markProperties: e.markProperties
};
});
if (this._currentCommand.commandStartMarker) {
Expand All @@ -540,6 +540,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
exitCode: undefined,
commandStartLineContent: undefined,
timestamp: 0,
markProperties: undefined
});
}
return {
Expand Down Expand Up @@ -581,7 +582,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
exitCode: e.exitCode,
hasOutput: () => !executedMarker?.isDisposed && !endMarker?.isDisposed && !!(executedMarker && endMarker && executedMarker.line < endMarker.line),
getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer),
genericMarkProperties: e.genericMarkProperties
markProperties: e.markProperties
};
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Event } from 'vs/base/common/event';
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
import { URI, UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ISerializableEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariable';

Expand Down
26 changes: 0 additions & 26 deletions src/vs/platform/terminal/common/terminalProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,3 @@ export interface ReplayEntry {
rows: number;
data: string;
}
export interface ISerializedCommand {
command: string;
cwd: string | undefined;
startLine: number | undefined;
startX: number | undefined;
endLine: number | undefined;
executedLine: number | undefined;
exitCode: number | undefined;
commandStartLineContent: string | undefined;
timestamp: number;
genericMarkProperties?: IGenericMarkProperties;
}

export interface IGenericMarkProperties {
hoverMessage?: string;
disableCommandStorage?: boolean;
}

export interface ISerializedCommandDetectionCapability {
isWindowsPty: boolean;
commands: ISerializedCommand[];
}
export interface IPtyHostProcessReplayEvent {
events: ReplayEntry[];
commands: ISerializedCommandDetectionCapability;
}
3 changes: 2 additions & 1 deletion src/vs/platform/terminal/common/terminalRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IPtyHostProcessReplayEvent, ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';

const MAX_RECORDER_DATA_SIZE = 1024 * 1024; // 1MB

Expand Down
54 changes: 49 additions & 5 deletions src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/l
import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
import { CwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/cwdDetectionCapability';
import { ICommandDetectionCapability, ICwdDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IBufferMarkCapability, ICommandDetectionCapability, ICwdDetectionCapability, ISerializedCommandDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { PartialCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/partialCommandDetectionCapability';
import { ILogService } from 'vs/platform/log/common/log';
// Importing types is safe in any layer
// eslint-disable-next-line local/code-import-patterns
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 { BufferMarkCapability } from 'vs/platform/terminal/common/capabilities/bufferMarkCapability';
// Importing types is safe in any layer
// eslint-disable-next-line local/code-import-patterns
import type { ITerminalAddon, Terminal } from 'xterm-headless';
import { URI } from 'vs/base/common/uri';


Expand Down Expand Up @@ -152,7 +154,16 @@ const enum VSCodeOscPt {
*
* WARNING: Any other properties may be changed and are not guaranteed to work in the future.
*/
Property = 'P'
Property = 'P',

/**
* Sets a mark/point-of-interest in the buffer. `OSC 633 ; SetMark [; Id=<string>] [; Hidden]`
* `Id` - The identifier of the mark that can be used to reference it
* `Hidden` - When set, the mark will be available to reference internally but will not visible
*
* WARNING: This sequence is unfinalized, DO NOT use this in your shell integration script.
*/
SetMark = 'SetMark',
}

/**
Expand Down Expand Up @@ -355,10 +366,19 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
return true;
}
case 'Task': {
this._createOrGetBufferMarkDetection(this._terminal);
this.capabilities.get(TerminalCapability.CommandDetection)?.setIsCommandStorageDisabled();
return true;
}
}
}
case VSCodeOscPt.SetMark: {
if (args.length > 2) {
return false;
}
this._createOrGetBufferMarkDetection(this._terminal).addMark(parseMarkSequence(args));
return true;
}
}

// Unrecognized sequence
Expand All @@ -379,7 +399,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
const [command] = data.split(';');
switch (command) {
case ITermOscPt.SetMark: {
this._createOrGetCommandDetection(this._terminal).handleGenericCommand({ genericMarkProperties: { disableCommandStorage: true } });
this._createOrGetBufferMarkDetection(this._terminal).addMark();
}
default: {
// Checking for known `<key>=<value>` pairs.
Expand Down Expand Up @@ -479,6 +499,15 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati
}
return commandDetection;
}

protected _createOrGetBufferMarkDetection(terminal: Terminal): IBufferMarkCapability {
let bufferMarkDetection = this.capabilities.get(TerminalCapability.BufferMarkDetection);
if (!bufferMarkDetection) {
bufferMarkDetection = new BufferMarkCapability(terminal);
this.capabilities.add(TerminalCapability.BufferMarkDetection, bufferMarkDetection);
}
return bufferMarkDetection;
}
}

export function deserializeMessage(message: string): string {
Expand All @@ -505,3 +534,18 @@ export function parseKeyValueAssignment(message: string): { key: string; value:
value: deserialized.substring(1 + separatorIndex)
};
}


export function parseMarkSequence(sequence: string[]): { id?: string; hidden?: boolean } {
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 };
}
Loading

0 comments on commit 9e0db45

Please sign in to comment.