Merge pull request #197846 from microsoft/tyriar/197735__197840

Reveal whole command in command nav and sticky scroll
This commit is contained in:
Daniel Imms
2023-11-09 11:04:27 -08:00
committed by GitHub
5 changed files with 103 additions and 44 deletions
@@ -886,6 +886,22 @@ export function getLinesForCommand(buffer: IBuffer, command: ITerminalCommand, c
return lines;
}
export function getPromptRowCount(command: ITerminalCommand, buffer: IBuffer): number {
if (!command.marker) {
return 1;
}
let promptRowCount = 1;
let promptStartLine = command.marker.line;
if (command.promptStartMarker) {
promptStartLine = Math.min(command.promptStartMarker?.line ?? command.marker.line, command.marker.line);
// Trim any leading whitespace-only lines to retain vertical space
while (promptStartLine < command.marker.line && (buffer.getLine(promptStartLine)?.translateToString(true) ?? '').length === 0) {
promptStartLine++;
}
promptRowCount = command.marker.line - promptStartLine + 1;
}
return promptRowCount;
}
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
// Cap the maximum number of lines generated to prevent potential performance problems. This is
@@ -502,11 +502,19 @@
}
.terminal-scroll-highlight-outline {
border: 1px solid #ffffff;
border-left: 1px solid #ffffff;
border-right: 1px solid #ffffff;
pointer-events: none;
}
.terminal-scroll-highlight-outline.top {
border-top: 1px solid #ffffff;
}
.terminal-scroll-highlight-outline.bottom {
border-bottom: 1px solid #ffffff;
}
.terminal-scroll-highlight {
.terminal-scroll-highlight,
.terminal-scroll-highlight.terminal-scroll-highlight-outline {
border-color: var(--vscode-focusBorder);
}
.hc-black .xterm-find-result-decoration,
@@ -110,6 +110,7 @@ export interface IMarkTracker {
scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void;
scrollToLine(line: number, position: ScrollPosition): void;
revealCommand(command: ITerminalCommand, position?: ScrollPosition): void;
registerTemporaryDecoration(marker: IMarker, endMarker?: IMarker): void;
}
@@ -6,12 +6,13 @@
import { coalesce } from 'vs/base/common/arrays';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { IMarkTracker } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import type { Terminal, IMarker, ITerminalAddon, IDecoration } from '@xterm/xterm';
import { timeout } from 'vs/base/common/async';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { getWindow } from 'vs/base/browser/dom';
import { getPromptRowCount } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
enum Boundary {
Top,
@@ -67,6 +68,14 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
return markers;
}
private _findCommand(marker: IMarker): ITerminalCommand | undefined {
const commandCapability = this._capabilities.get(TerminalCapability.CommandDetection);
if (commandCapability) {
return commandCapability.commands.find(e => e.marker?.line === marker.line);
}
return undefined;
}
clearMarker(): void {
// Clear the current marker so successive focus/selection actions are performed from the
// bottom of the buffer
@@ -137,7 +146,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
}
this._currentMarker = this._getMarkers(skipEmptyCommands)[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
this._scrollToCommand(this._currentMarker, scrollPosition);
}
scrollToNextMark(scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false, skipEmptyCommands: boolean = true): void {
@@ -183,43 +192,65 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
}
this._currentMarker = this._getMarkers(skipEmptyCommands)[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
this._scrollToCommand(this._currentMarker, scrollPosition);
}
private _scrollToMarker(marker: IMarker, position: ScrollPosition, endMarker?: IMarker, hideDecoration?: boolean): void {
private _scrollToCommand(marker: IMarker, position: ScrollPosition): void {
const command = this._findCommand(marker);
if (command) {
this.revealCommand(command, position);
} else {
this._scrollToMarker(marker, position);
}
}
private _scrollToMarker(start: IMarker | number, position: ScrollPosition, end?: IMarker | number, hideDecoration?: boolean): void {
if (!this._terminal) {
return;
}
if (!this._isMarkerInViewport(this._terminal, marker)) {
const line = this.getTargetScrollLine(marker.line, position);
if (!this._isMarkerInViewport(this._terminal, start)) {
const line = this.getTargetScrollLine(toLineIndex(start), position);
this._terminal.scrollToLine(line);
}
if (!hideDecoration) {
this.registerTemporaryDecoration(marker, endMarker);
this.registerTemporaryDecoration(start, end);
}
}
private _createMarkerForOffset(marker: IMarker, offset: number): IMarker {
if (offset === 0) {
private _createMarkerForOffset(marker: IMarker | number, offset: number): IMarker {
if (offset === 0 && isMarker(marker)) {
return marker;
} else {
const offsetMarker = this._terminal?.registerMarker(-this._terminal.buffer.active.cursorY + marker.line - this._terminal.buffer.active.baseY + offset);
const offsetMarker = this._terminal?.registerMarker(-this._terminal.buffer.active.cursorY + toLineIndex(marker) - this._terminal.buffer.active.baseY + offset);
if (offsetMarker) {
return offsetMarker;
} else {
throw new Error(`Could not register marker with offset ${marker.line}, ${offset}`);
throw new Error(`Could not register marker with offset ${toLineIndex(marker)}, ${offset}`);
}
}
}
registerTemporaryDecoration(marker: IMarker, endMarker?: IMarker): void {
revealCommand(command: ITerminalCommand, position: ScrollPosition = ScrollPosition.Middle): void {
if (!this._terminal || !command.marker) {
return;
}
const promptRowCount = getPromptRowCount(command, this._terminal.buffer.active);
this._scrollToMarker(
command.marker.line - (promptRowCount - 1),
position,
command.marker
);
}
registerTemporaryDecoration(marker: IMarker | number, endMarker?: IMarker | number): void {
if (!this._terminal) {
return;
}
this._resetNavigationDecorations();
const color = this._themeService.getColorTheme().getColor(TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR);
const startLine = marker.line;
const decorationCount = endMarker ? endMarker.line - startLine + 1 : 1;
const startLine = toLineIndex(marker);
const decorationCount = endMarker ? toLineIndex(endMarker) - startLine + 1 : 1;
for (let i = 0; i < decorationCount; i++) {
const decoration = this._terminal.registerDecoration({
@@ -236,14 +267,18 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
decoration.onRender(element => {
if (!renderedElement) {
renderedElement = element;
if (decorationCount > 1) {
element.classList.add('terminal-scroll-highlight');
} else {
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
if (i === 0) {
element.classList.add('top');
}
if (this._terminal?.element) {
element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`;
if (i === decorationCount - 1) {
element.classList.add('bottom');
}
} else {
element.classList.add('terminal-scroll-highlight', 'terminal-scroll-highlight-outline');
}
if (this._terminal?.element) {
element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`;
}
});
decoration.onDispose(() => { this._navigationDecorations = this._navigationDecorations?.filter(d => d !== decoration); });
@@ -270,9 +305,10 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe
return line;
}
private _isMarkerInViewport(terminal: Terminal, marker: IMarker) {
private _isMarkerInViewport(terminal: Terminal, marker: IMarker | number) {
const viewportY = terminal.buffer.active.viewportY;
return marker.line >= viewportY && marker.line < viewportY + terminal.rows;
const line = toLineIndex(marker);
return line >= viewportY && line < viewportY + terminal.rows;
}
scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void {
@@ -474,3 +510,11 @@ export function selectLines(xterm: Terminal, start: IMarker | Boundary, end: IMa
xterm.selectLines(startLine, endLine);
}
function isMarker(value: IMarker | number): value is IMarker {
return typeof value !== 'number';
}
function toLineIndex(line: IMarker | number): number {
return isMarker(line) ? line.line : line;
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type { CanvasAddon as CanvasAddonType } from '@xterm/addon-canvas';
import type { SerializeAddon as SerializeAddonType } from '@xterm/addon-serialize';
import type { IMarker, ITerminalOptions, Terminal as RawXtermTerminal, Terminal as XTermTerminal } from '@xterm/xterm';
import type { ITerminalOptions, Terminal as RawXtermTerminal, Terminal as XTermTerminal } from '@xterm/xterm';
import { importAMDNodeModule } from 'vs/amdX';
import { $, addStandardDisposableListener } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
@@ -14,10 +14,10 @@ import { Disposable, MutableDisposable, combinedDisposable, toDisposable } from
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';
import { ICommandDetectionCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
import { getPromptRowCount } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IXtermColorProvider, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon';
import { TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { terminalStickyScrollHoverBackground } from 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollColorRegistry';
@@ -39,7 +39,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
private _pendingCanvasAddon?: CancelablePromise<void>;
private _element?: HTMLElement;
private _currentStickyMarker?: IMarker;
private _currentStickyCommand?: ITerminalCommand;
private _currentContent?: string;
private _refreshListeners = this._register(new MutableDisposable());
@@ -142,7 +142,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
// The command from viewportY + 1 is used because this one will not be obscured by sticky
// scroll.
const command = this._commandDetection.getCommandForLine(this._xterm.raw.buffer.active.viewportY + 1);
this._currentStickyMarker = undefined;
this._currentStickyCommand = undefined;
// Sticky scroll only works with non-partial commands
if (!command || !('marker' in command)) {
@@ -166,21 +166,12 @@ export class TerminalStickyScrollOverlay extends Disposable {
// 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;
}
const promptRowCount = getPromptRowCount(command, this._xterm.raw.buffer.active);
// 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 - promptStartLine
scrollback: this._xterm.raw.buffer.active.baseY - marker.line + (promptRowCount - 1)
});
// Write content if it differs
@@ -197,7 +188,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
}
if (content && command.exitCode !== undefined) {
this._currentStickyMarker = marker;
this._currentStickyCommand = command;
this._setVisible(true);
} else {
this._setVisible(false);
@@ -228,9 +219,8 @@ export class TerminalStickyScrollOverlay extends Disposable {
// Scroll to the command on click
this._register(addStandardDisposableListener(hoverOverlay, 'click', () => {
if (this._xterm && this._currentStickyMarker) {
this._xterm.scrollToLine(this._currentStickyMarker.line, ScrollPosition.Middle);
this._xterm.markTracker.registerTemporaryDecoration(this._currentStickyMarker);
if (this._xterm && this._currentStickyCommand) {
this._xterm.markTracker.revealCommand(this._currentStickyCommand);
}
}));