mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 13:29:44 +00:00
Add terminal sticky scroll setting
This commit is contained in:
parent
fa39026a87
commit
80e6659815
|
@ -118,6 +118,7 @@ export const enum TerminalSettingId {
|
|||
FocusAfterRun = 'terminal.integrated.focusAfterRun',
|
||||
AccessibleViewPreserveCursorPosition = 'terminal.integrated.accessibleViewPreserveCursorPosition',
|
||||
AccessibleViewFocusOnCommandExecution = 'terminal.integrated.accessibleViewFocusOnCommandExecution',
|
||||
EnableStickyScroll = 'terminal.integrated.enableStickyScroll',
|
||||
|
||||
// Debug settings that are hidden from user
|
||||
|
||||
|
|
|
@ -625,6 +625,13 @@ const terminalConfiguration: IConfigurationNode = {
|
|||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
[TerminalSettingId.EnableStickyScroll]: {
|
||||
markdownDescription: localize('terminal.integrated.enableStickyScroll', "Experimental: Whether to enable the sticky scroll overlay at the top of the terminal."),
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// TODO: Prevent setting at folder level after it becomes stable,
|
||||
// scope: ConfigurationScope.RESOURCE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { Terminal as RawXtermTerminal } from '@xterm/xterm';
|
||||
import { IDimension } from 'vs/base/browser/dom';
|
||||
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./media/stickyScroll';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
|
||||
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
|
||||
import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions';
|
||||
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
|
||||
|
@ -32,11 +35,17 @@ class TerminalStickyScrollContribution extends Disposable implements ITerminalCo
|
|||
private readonly _instance: ITerminalInstance,
|
||||
processManager: ITerminalProcessManager | ITerminalProcessInfo,
|
||||
widgetManager: TerminalWidgetManager,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._refreshState();
|
||||
this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(TerminalSettingId.EnableStickyScroll)) {
|
||||
this._refreshState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
|
||||
|
@ -44,6 +53,10 @@ class TerminalStickyScrollContribution extends Disposable implements ITerminalCo
|
|||
this._refreshState();
|
||||
}
|
||||
|
||||
layout(xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void {
|
||||
this._refreshState();
|
||||
}
|
||||
|
||||
private _refreshState(): void {
|
||||
if (this._overlay.value) {
|
||||
this._tryDisable();
|
||||
|
@ -73,18 +86,22 @@ class TerminalStickyScrollContribution extends Disposable implements ITerminalCo
|
|||
}
|
||||
|
||||
private _tryEnable(): void {
|
||||
const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection);
|
||||
if (capability && this._xterm) {
|
||||
this._overlay.value = this._instantiationService.createInstance(TerminalStickyScrollOverlay, this._xterm, capability);
|
||||
if (this._shouldBeEnabled()) {
|
||||
// TODO: Ensure open has happened to prevent race condition where not attached
|
||||
this._overlay.value = this._instantiationService.createInstance(TerminalStickyScrollOverlay, this._xterm!, this._instance.capabilities.get(TerminalCapability.CommandDetection)!);
|
||||
}
|
||||
}
|
||||
|
||||
private _tryDisable(): void {
|
||||
const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection);
|
||||
if (!capability || !this._xterm) {
|
||||
if (!this._shouldBeEnabled()) {
|
||||
this._overlay.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldBeEnabled(): boolean {
|
||||
const capability = this._instance.capabilities.get(TerminalCapability.CommandDetection);
|
||||
return !!(this._configurationService.getValue(TerminalSettingId.EnableStickyScroll) && capability && this._xterm?.raw?.element);
|
||||
}
|
||||
}
|
||||
|
||||
registerTerminalContribution(TerminalStickyScrollContribution.ID, TerminalStickyScrollContribution, true);
|
||||
|
|
|
@ -7,10 +7,11 @@ 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 } from '@xterm/xterm';
|
||||
import { importAMDNodeModule } from 'vs/amdX';
|
||||
import { $, addStandardDisposableListener, append } from 'vs/base/browser/dom';
|
||||
import { $, addStandardDisposableListener } from 'vs/base/browser/dom';
|
||||
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 } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./media/stickyScroll';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
@ -38,7 +39,9 @@ const enum CssClasses {
|
|||
export class TerminalStickyScrollOverlay extends Disposable {
|
||||
private _stickyScrollOverlay?: RawXtermTerminal;
|
||||
private _serializeAddon?: SerializeAddonType;
|
||||
private _canvasAddon?: CanvasAddonType;
|
||||
|
||||
private _canvasAddon = new MutableDisposable<CanvasAddonType>();
|
||||
private _pendingCanvasAddon?: CancelablePromise<void>;
|
||||
|
||||
private _element?: HTMLElement;
|
||||
private _currentStickyMarker?: IMarker;
|
||||
|
@ -62,6 +65,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
this._setState((buffer ?? this._xterm.raw.buffer.active).type === 'normal' ? OverlayState.On : OverlayState.Off);
|
||||
}));
|
||||
|
||||
// React to option changes
|
||||
this._register(configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(TERMINAL_CONFIG_SECTION)) {
|
||||
this._syncOptions();
|
||||
|
@ -73,26 +77,21 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
|
||||
// Eagerly create the overlay
|
||||
TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService).then(ctor => {
|
||||
const overlay = new ctor({
|
||||
this._stickyScrollOverlay = new ctor({
|
||||
rows: 1,
|
||||
cols: this._xterm.raw.cols,
|
||||
allowProposedApi: true,
|
||||
...this._getOptions()
|
||||
});
|
||||
this._stickyScrollOverlay = overlay;
|
||||
|
||||
this._getSerializeAddonConstructor().then(SerializeAddon => {
|
||||
this._serializeAddon = new SerializeAddon();
|
||||
this._xterm.raw.loadAddon(this._serializeAddon);
|
||||
// Trigger a render as the serialize addon is required to render
|
||||
this._refresh();
|
||||
});
|
||||
|
||||
// TODO: Sync every render
|
||||
if (this._xterm.isGpuAccelerated) {
|
||||
this._getCanvasAddonConstructor().then(CanvasAddon => {
|
||||
this._canvasAddon = new CanvasAddon();
|
||||
overlay.loadAddon(this._canvasAddon);
|
||||
});
|
||||
}
|
||||
this._syncGpuAccelerationState();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -119,6 +118,7 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
this._refreshListeners.value = combinedDisposable(
|
||||
this._xterm.raw.onScroll(() => this._refresh()),
|
||||
this._xterm.raw.onLineFeed(() => this._refresh()),
|
||||
// TODO: This may not exist!!!
|
||||
addStandardDisposableListener(this._xterm.raw.element!.querySelector('.xterm-viewport')!, 'scroll', () => this._refresh()),
|
||||
);
|
||||
}
|
||||
|
@ -131,13 +131,16 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
private _setVisible(isVisible: boolean) {
|
||||
if (isVisible) {
|
||||
this._ensureElement();
|
||||
// The GPU acceleration state may be changes at any time and there is no event to listen
|
||||
// to currently.
|
||||
this._syncGpuAccelerationState();
|
||||
}
|
||||
this._element?.classList.toggle(CssClasses.Visible, isVisible);
|
||||
}
|
||||
|
||||
@throttle(0)
|
||||
private _refresh(): void {
|
||||
if (!this._xterm?.raw?.element) {
|
||||
if (!this._xterm?.raw?.element || !this._serializeAddon) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -164,17 +167,19 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
this._setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Support multi-line prompts
|
||||
// TODO: Support multi-line commands
|
||||
|
||||
if (this._stickyScrollOverlay) {
|
||||
this._stickyScrollOverlay.write('\x1b[H\x1b[K');
|
||||
// Clear attrs, reset cursor position, clear right
|
||||
this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[K');
|
||||
// TODO: Serializing all content up to the required line is inefficient; support providing single line/range serialize addon
|
||||
const s = this._serializeAddon?.serialize({
|
||||
const s = this._serializeAddon.serialize({
|
||||
scrollback: this._xterm.raw.buffer.active.baseY - marker.line
|
||||
});
|
||||
const content = s ? s.substring(0, s.indexOf('\r')) : undefined;
|
||||
|
||||
// TODO: Don't write if it's the same
|
||||
if (content) {
|
||||
this._stickyScrollOverlay.write(content);
|
||||
// Debug log to show the command
|
||||
|
@ -207,10 +212,12 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
const overlay = this._stickyScrollOverlay;
|
||||
|
||||
this._element = $('.terminal-sticky-scroll');
|
||||
// // TODO: Safety
|
||||
this._xterm!.raw.element!.parentElement!.append(this._element);
|
||||
|
||||
const hoverOverlay = $('.hover-overlay');
|
||||
this._element.append(hoverOverlay);
|
||||
this._xterm.raw.element.parentElement.append(this._element);
|
||||
this._register(toDisposable(() => this._element?.remove()));
|
||||
|
||||
this._stickyScrollOverlay.open(this._element);
|
||||
|
||||
// Scroll to the command on click
|
||||
this._register(addStandardDisposableListener(hoverOverlay, 'click', () => {
|
||||
|
@ -224,21 +231,43 @@ export class TerminalStickyScrollOverlay extends Disposable {
|
|||
// hover state as the selection is inaccessible anyway
|
||||
this._register(addStandardDisposableListener(hoverOverlay, 'mouseover', () => overlay.selectAll()));
|
||||
this._register(addStandardDisposableListener(hoverOverlay, 'mouseleave', () => overlay.clearSelection()));
|
||||
|
||||
// TODO: Add to a container outside the xterm instance?
|
||||
// TODO: Remove !
|
||||
this._stickyScrollOverlay.open(this._element);
|
||||
|
||||
append(this._element, hoverOverlay);
|
||||
}
|
||||
|
||||
@throttle(0)
|
||||
private _syncOptions(): void {
|
||||
private _syncOptions() {
|
||||
if (!this._stickyScrollOverlay) {
|
||||
return;
|
||||
}
|
||||
this._stickyScrollOverlay.resize(this._xterm.raw.cols, 1);
|
||||
this._stickyScrollOverlay.options = this._getOptions();
|
||||
this._syncGpuAccelerationState();
|
||||
}
|
||||
|
||||
private _syncGpuAccelerationState() {
|
||||
if (!this._stickyScrollOverlay) {
|
||||
return;
|
||||
}
|
||||
const overlay = this._stickyScrollOverlay;
|
||||
|
||||
// The Webgl renderer isn't used here as there are a limited number of webgl contexts
|
||||
// available within a given page. This is a single row that isn't rendered to often so the
|
||||
// performance isn't as important
|
||||
if (this._xterm.isGpuAccelerated) {
|
||||
if (!this._canvasAddon.value && !this._pendingCanvasAddon) {
|
||||
this._pendingCanvasAddon = createCancelablePromise(async token => {
|
||||
const CanvasAddon = await this._getCanvasAddonConstructor();
|
||||
if (!token.isCancellationRequested) {
|
||||
this._canvasAddon.value = new CanvasAddon();
|
||||
overlay.loadAddon(this._canvasAddon.value);
|
||||
}
|
||||
this._pendingCanvasAddon = undefined;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._canvasAddon.clear();
|
||||
this._pendingCanvasAddon?.cancel();
|
||||
this._pendingCanvasAddon = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _getOptions(): ITerminalOptions {
|
||||
|
|
Loading…
Reference in a new issue