mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-12 11:39:57 +01:00
Merge pull request #197846 from microsoft/tyriar/197735__197840
Reveal whole command in command nav and sticky scroll
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+10
-20
@@ -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);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user