Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ export class TerminalStickyScrollOverlay extends Disposable {
const buffer = xterm.buffer.active;
const promptRowCount = command.getPromptRowCount();
const commandRowCount = command.getCommandRowCount();
const stickyScrollLineStart = startMarker.line - (promptRowCount - 1);

// Calculate the row offset, this is the number of rows that will be clipped from the top
// of the sticky overlay because we do not want to show any content above the bounds of the
Expand All @@ -288,11 +287,11 @@ export class TerminalStickyScrollOverlay extends Disposable {
const isPartialCommand = !isFullTerminalCommand(command);
const rowOffset = !isPartialCommand && command.endMarker ? Math.max(buffer.viewportY - command.endMarker.line + 1, 0) : 0;
const maxLineCount = Math.min(this._rawMaxLineCount, Math.floor(xterm.rows * Constants.StickyScrollPercentageCap));
const stickyScrollLineCount = Math.min(promptRowCount + commandRowCount - 1, maxLineCount) - rowOffset;
const isTruncated = stickyScrollLineCount < promptRowCount + commandRowCount - 1;
const { lineStart: stickyScrollLineStart, lineCount: stickyScrollLineCount, isTruncated } = getStickyScrollLayout(startMarker.line, promptRowCount, commandRowCount, maxLineCount, rowOffset);

// Hide sticky scroll if it's currently on a line that contains it
if (buffer.viewportY <= stickyScrollLineStart) {
// Hide sticky scroll if there is nothing to show or if it's currently on a line that
// contains it
if (stickyScrollLineCount <= 0 || buffer.viewportY <= stickyScrollLineStart) {
this._setVisible(false);
return;
}
Expand Down Expand Up @@ -554,3 +553,39 @@ function lineStartsWith(line: IBufferLine | undefined, text: string): boolean {
}
return true;
}

/**
* Determines what part of a command's prompt and command line to show in the sticky scroll overlay.
*
* The command line is prioritized over the prompt, only including the prompt when the prompt and
* the command line both fit within `maxLineCount`. This matters because the rows above the command
* line are not necessarily prompt rows, for example output printed in between the previous command
* finishing and the prompt being drawn (job control notifications, background processes, etc.) is
* considered part of the prompt. Including those rows regardless would push the command line out of
* the overlay, showing only output and hiding the command the output belongs to.
*
* @param commandStartLine The buffer line the command line starts on.
* @param promptRowCount The number of rows the prompt spans, including the command start row.
* @param commandRowCount The number of rows the command line spans, including the command start row.
* @param maxLineCount The maximum number of rows the overlay is allowed to show.
* @param rowOffset The number of rows to clip from the top of the overlay. A `lineCount` of 0 means
* the row offset clipped all rows and there is nothing to show.
*/
export function getStickyScrollLayout(
commandStartLine: number,
promptRowCount: number,
commandRowCount: number,
maxLineCount: number,
rowOffset: number
): { lineStart: number; lineCount: number; isTruncated: boolean } {
// The prompt and the command line share the command start row
const promptAndCommandRowCount = promptRowCount + commandRowCount - 1;
const includePrompt = promptAndCommandRowCount <= maxLineCount;
const contentRowCount = includePrompt ? promptAndCommandRowCount : commandRowCount;
const lineCount = Math.max(Math.min(contentRowCount, maxLineCount) - rowOffset, 0);
return {
lineStart: includePrompt ? commandStartLine - (promptRowCount - 1) : commandStartLine,
lineCount,
isTruncated: lineCount < contentRowCount
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* 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 } from '@xterm/xterm';
import { deepStrictEqual } from 'assert';
import { importAMDNodeModule } from '../../../../../../amdX.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { CommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js';
import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js';
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
import { writeP } from '../../../../terminal/browser/terminalTestHelpers.js';
import { getStickyScrollLayout } from '../../browser/terminalStickyScrollOverlay.js';

suite('TerminalStickyScrollOverlay', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();

suite('getStickyScrollLayout', () => {
test('should show the prompt and the command line when they fit', () => {
deepStrictEqual(getStickyScrollLayout(10, 2, 1, 5, 0), { lineStart: 9, lineCount: 2, isTruncated: false });
});

test('should truncate the command line when it does not fit', () => {
deepStrictEqual(getStickyScrollLayout(10, 1, 8, 5, 0), { lineStart: 10, lineCount: 5, isTruncated: true });
});

test('should drop the prompt when the prompt and the command line do not fit', () => {
deepStrictEqual(getStickyScrollLayout(10, 6, 1, 5, 0), { lineStart: 10, lineCount: 1, isTruncated: false });
});

test('should prefer the command line rows over the prompt rows', () => {
deepStrictEqual(getStickyScrollLayout(10, 2, 5, 5, 0), { lineStart: 10, lineCount: 5, isTruncated: false });
});

test('should clip rows from the top of the overlay for the row offset', () => {
deepStrictEqual(getStickyScrollLayout(10, 3, 1, 5, 1), { lineStart: 8, lineCount: 2, isTruncated: true });
});

test('should show nothing when the row offset clips all rows', () => {
deepStrictEqual(getStickyScrollLayout(10, 6, 1, 5, 1), { lineStart: 10, lineCount: 0, isTruncated: true });
});
});

suite('command detection', () => {
let xterm: Terminal;
let capability: CommandDetectionCapability;

setup(async () => {
const TerminalCtor = (await importAMDNodeModule<typeof import('@xterm/xterm')>('@xterm/xterm', 'lib/xterm.js')).Terminal;
xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30, logger: TestXtermLogger }));
const instantiationService = workbenchInstantiationService(undefined, store);
capability = store.add(instantiationService.createInstance(CommandDetectionCapability, xterm));
});

test('should show the command line when output is printed after the previous command finished', async () => {
// A command finishes and more output is printed before the prompt is drawn. The prompt
// start is reported at the previous command's end, so that output is treated as part
// of the prompt of the following command.
capability.handlePromptStart();
await writeP(xterm, '\r$ ');
capability.handleCommandStart();
await writeP(xterm, 'npm install');
capability.handleCommandExecuted();
await writeP(xterm, '\r\nadded 100 packages\r\n');
capability.handleCommandFinished(0);
await writeP(xterm, 'output printed after the command finished\r\n'.repeat(5));

capability.handlePromptStart();
await writeP(xterm, '$ ');
capability.handleCommandStart();
await writeP(xterm, './scripts/code.sh');
capability.handleCommandExecuted();

const command = capability.currentCommand!;
const commandStartLine = command.commandStartMarker!.line;
const promptRowCount = command.getPromptRowCount();
deepStrictEqual(
{ promptRowCount, ...getStickyScrollLayout(commandStartLine, promptRowCount, command.getCommandRowCount(), 5, 0) },
{ promptRowCount: 6, lineStart: commandStartLine, lineCount: 1, isTruncated: false }
);
});
});
});