mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-26 19:36:52 +01:00
Merge pull request #197835 from microsoft/tyriar/197735
Implement terminal multi-line prompt stick scroll
This commit is contained in:
@@ -167,6 +167,23 @@ export function stripWildcards(pattern: string): string {
|
||||
return pattern.replace(/\*/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the nth occurrence of a character within a string.
|
||||
* @param text The text to search.
|
||||
* @param char The character to search for.
|
||||
* @param n The number of chars to find
|
||||
*/
|
||||
export function findNthOccurrenceIndex(text: string, char: string, n: number): number {
|
||||
let index = -1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
index = text.indexOf(char, index + 1);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export interface RegExpOptions {
|
||||
matchCase?: boolean;
|
||||
wholeWord?: boolean;
|
||||
|
||||
@@ -252,6 +252,7 @@ interface IBaseTerminalCommand {
|
||||
|
||||
export interface ITerminalCommand extends IBaseTerminalCommand {
|
||||
// Optional non-serializable
|
||||
promptStartMarker?: IMarker;
|
||||
marker?: IXtermMarker;
|
||||
endMarker?: IXtermMarker;
|
||||
executedMarker?: IXtermMarker;
|
||||
@@ -266,6 +267,7 @@ export interface ITerminalCommand extends IBaseTerminalCommand {
|
||||
export interface ISerializedTerminalCommand extends IBaseTerminalCommand {
|
||||
// Optional non-serializable converted for serialization
|
||||
startLine: number | undefined;
|
||||
promptStartLine: number | undefined;
|
||||
startX: number | undefined;
|
||||
endLine: number | undefined;
|
||||
executedLine: number | undefined;
|
||||
|
||||
@@ -611,6 +611,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
const newCommand: ITerminalCommand = {
|
||||
command: this._handleCommandStartOptions?.ignoreCommandLine ? '' : (command || ''),
|
||||
isTrusted: !!this._currentCommand.isTrusted,
|
||||
promptStartMarker: this._currentCommand.promptStartMarker,
|
||||
marker: this._currentCommand.commandStartMarker,
|
||||
endMarker,
|
||||
executedMarker,
|
||||
@@ -683,6 +684,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
serialize(): ISerializedCommandDetectionCapability {
|
||||
const commands: ISerializedTerminalCommand[] = this.commands.map(e => {
|
||||
return {
|
||||
promptStartLine: e.promptStartMarker?.line,
|
||||
startLine: e.marker?.line,
|
||||
startX: undefined,
|
||||
endLine: e.endMarker?.line,
|
||||
@@ -699,6 +701,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
});
|
||||
if (this._currentCommand.commandStartMarker) {
|
||||
commands.push({
|
||||
promptStartLine: this._currentCommand.promptStartMarker?.line,
|
||||
startLine: this._currentCommand.commandStartMarker.line,
|
||||
startX: this._currentCommand.commandStartX,
|
||||
endLine: undefined,
|
||||
@@ -729,10 +732,14 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
if (!marker) {
|
||||
continue;
|
||||
}
|
||||
const promptStartMarker = e.promptStartLine !== undefined ? this._terminal.registerMarker(e.promptStartLine - (buffer.baseY + buffer.cursorY)) : undefined;
|
||||
// Partial command
|
||||
if (!e.endLine) {
|
||||
this._currentCommand.commandStartMarker = marker;
|
||||
this._currentCommand.commandStartX = e.startX;
|
||||
if (promptStartMarker) {
|
||||
this._currentCommand.promptStartMarker = promptStartMarker;
|
||||
}
|
||||
this._cwd = e.cwd;
|
||||
this._onCommandStarted.fire({ marker } as ITerminalCommand);
|
||||
continue;
|
||||
@@ -743,6 +750,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
|
||||
const newCommand: ITerminalCommand = {
|
||||
command: this.__isCommandStorageDisabled ? '' : e.command,
|
||||
isTrusted: e.isTrusted,
|
||||
promptStartMarker,
|
||||
marker,
|
||||
endMarker,
|
||||
executedMarker,
|
||||
|
||||
+19
-4
@@ -11,6 +11,7 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async
|
||||
import { memoize, throttle } from 'vs/base/common/decorators';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Disposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { findNthOccurrenceIndex } from 'vs/base/common/strings';
|
||||
import 'vs/css!./media/stickyScroll';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
|
||||
@@ -162,19 +163,33 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Support multi-line prompts
|
||||
// TODO: Support multi-line commands
|
||||
|
||||
// Determine prompt length
|
||||
let promptRowCount = 1;
|
||||
let promptStartLine = marker.line;
|
||||
if (command.promptStartMarker) {
|
||||
promptStartLine = Math.min(command.promptStartMarker?.line ?? marker.line, marker.line);
|
||||
// Trim any leading whitespace-only lines to retain vertical space
|
||||
while (promptStartLine < marker.line && (this._xterm.raw.buffer.active.getLine(promptStartLine)?.translateToString(true) ?? '').length === 0) {
|
||||
promptStartLine++;
|
||||
}
|
||||
promptRowCount = marker.line - promptStartLine + 1;
|
||||
}
|
||||
|
||||
// Clear attrs, reset cursor position, clear right
|
||||
// TODO: Serializing all content up to the required line is inefficient; support providing single line/range serialize addon
|
||||
const s = this._serializeAddon.serialize({
|
||||
scrollback: this._xterm.raw.buffer.active.baseY - marker.line
|
||||
scrollback: this._xterm.raw.buffer.active.baseY - promptStartLine
|
||||
});
|
||||
|
||||
// Write content if it differs
|
||||
const content = s ? s.substring(0, s.indexOf('\r')) : undefined;
|
||||
const content = s ? s.substring(0, findNthOccurrenceIndex(s, '\r', promptRowCount)) : undefined;
|
||||
if (content && this._currentContent !== content) {
|
||||
this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[K');
|
||||
if (this._stickyScrollOverlay.rows !== promptRowCount) {
|
||||
this._stickyScrollOverlay.resize(this._stickyScrollOverlay.cols, promptRowCount);
|
||||
}
|
||||
this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[2K');
|
||||
this._stickyScrollOverlay.write(content);
|
||||
this._currentContent = content;
|
||||
// Debug log to show the command
|
||||
|
||||
@@ -25,41 +25,66 @@ export function setup() {
|
||||
await settingsEditor.clearUserSettings();
|
||||
});
|
||||
|
||||
it('should show sticky scroll when appropriate', async () => {
|
||||
// A polling approach is used to avoid test flakiness. While it's not ideal that this
|
||||
// occurs, the main purpose of the tests is to verify sticky scroll shows and updates,
|
||||
// not edge case race conditions on terminal start up
|
||||
async function checkCommandAndOutput(
|
||||
command: string,
|
||||
exitCode: number,
|
||||
prompt: string = 'Prompt> ',
|
||||
expectedLineCount: number = 1
|
||||
): Promise<void> {
|
||||
const data = generateCommandAndOutput(prompt, command, exitCode);
|
||||
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, data);
|
||||
// Verify line count
|
||||
await app.code.waitForElements('.terminal-sticky-scroll .xterm-rows > *', true, e => e.length === expectedLineCount);
|
||||
// Verify content
|
||||
const element = await app.code.getElement('.terminal-sticky-scroll .xterm-rows');
|
||||
if (
|
||||
element &&
|
||||
// New lines don't come through in textContent
|
||||
element.textContent.indexOf(`${prompt.replace(/\\r\\n/g, '')}${command}`) >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Failed for command ${command}, exitcode ${exitCode}, text content ${element?.textContent}`);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create the simplest system profile to get as little process interaction as possible
|
||||
await terminal.createEmptyTerminal();
|
||||
});
|
||||
|
||||
function generateCommandAndOutput(command: string, exitCode: number): string {
|
||||
return [
|
||||
`${vsc('A')}Prompt> ${vsc('B')}${command}`,
|
||||
`\\r\\n${vsc('C')}`,
|
||||
`\\r\\ndata`.repeat(50),
|
||||
`\\r\\n${vsc(`D;${exitCode}`)}`,
|
||||
].join('');
|
||||
}
|
||||
|
||||
// A polling approach is used to avoid test flakiness. While it's not ideal that this
|
||||
// occurs, the main purpose of the tests is to verify sticky scroll shows and updates,
|
||||
// not edge case race conditions on terminal start up
|
||||
async function checkCommandAndOutput(command: string, exitCode: number): Promise<void> {
|
||||
const data = generateCommandAndOutput(command, exitCode);
|
||||
await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, data);
|
||||
const element = await app.code.getElement('.terminal-sticky-scroll .xterm-rows');
|
||||
if (element && element.textContent.indexOf(`Prompt> ${command}`) >= 0) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Failed for command ${command}, exitcode ${exitCode}, text content ${element?.textContent}`);
|
||||
}
|
||||
|
||||
it('should show sticky scroll when appropriate', async () => {
|
||||
// Write prompt, fill viewport, finish command, print new prompt, verify sticky scroll
|
||||
await checkCommandAndOutput('sticky scroll 1', 0);
|
||||
|
||||
// And again with a failed command
|
||||
await checkCommandAndOutput('sticky scroll 2', 1);
|
||||
});
|
||||
|
||||
it('should support multi-line prompt', async () => {
|
||||
// Standard multi-line prompt
|
||||
await checkCommandAndOutput('sticky scroll 1', 0, "Multi-line\\r\\nPrompt> ", 2);
|
||||
|
||||
// New line before prompt
|
||||
await checkCommandAndOutput('sticky scroll 2', 0, "\\r\\nMulti-line Prompt> ", 1);
|
||||
|
||||
// New line before multi-line prompt
|
||||
await checkCommandAndOutput('sticky scroll 3', 0, "\\r\\nMulti-line\\r\\nPrompt> ", 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateCommandAndOutput(prompt: string, command: string, exitCode: number): string {
|
||||
return [
|
||||
`${vsc('A')}${prompt}${vsc('B')}${command}`,
|
||||
`\\r\\n${vsc('C')}`,
|
||||
`\\r\\ndata`.repeat(50),
|
||||
`\\r\\n${vsc(`D;${exitCode}`)}`,
|
||||
].join('');
|
||||
}
|
||||
|
||||
function vsc(data: string) {
|
||||
return setTextParams(`633;${data}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user