Merge pull request #285039 from microsoft/tyriar/285032__285031

Fix performance problems with terminal tabs
This commit is contained in:
Daniel Imms 2025-12-25 13:31:00 -08:00 committed by GitHub
commit 6979e72216
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 82 additions and 80 deletions

View File

@ -90,7 +90,6 @@ import type { IMenu } from '../../../../platform/actions/common/actions.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { TerminalContribCommandId } from '../terminalContribExports.js';
import type { IProgressState } from '@xterm/addon-progress';
import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { PromptInputState } from '../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js';
import { hasKey, isNumber, isString } from '../../../../base/common/types.js';
@ -465,7 +464,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
capabilityListeners.get(e.id)?.dispose();
const refreshInfo = () => {
this._labelComputer?.refreshLabel(this);
refreshShellIntegrationInfoStatus(this);
this._refreshShellIntegrationInfoStatus(this);
};
switch (e.id) {
case TerminalCapability.CwdDetection: {
@ -499,7 +498,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
}
}
}));
this._register(this.onDidChangeShellType(() => refreshShellIntegrationInfoStatus(this)));
this._register(this.onDidChangeShellType(() => this._refreshShellIntegrationInfoStatus(this)));
this._register(this.capabilities.onDidRemoveCapability(e => {
capabilityListeners.get(e.id)?.dispose();
}));
@ -888,7 +887,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
// Register and update the terminal's shell integration status
this._register(Event.runAndSubscribe(xterm.shellIntegration.onDidChangeSeenSequences, () => {
if (xterm.shellIntegration.seenSequences.size > 0) {
refreshShellIntegrationInfoStatus(this);
this._refreshShellIntegrationInfoStatus(this);
}
}));
@ -922,6 +921,54 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
return xterm;
}
// Debounce this to avoid impacting input latency while typing into the prompt
@debounce(500)
private _refreshShellIntegrationInfoStatus(instance: ITerminalInstance) {
if (!instance.xterm) {
return;
}
const cmdDetectionType = (
instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection
? nls.localize('shellIntegration.rich', 'Rich')
: instance.capabilities.has(TerminalCapability.CommandDetection)
? nls.localize('shellIntegration.basic', 'Basic')
: instance.usedShellIntegrationInjection
? nls.localize('shellIntegration.injectionFailed', "Injection failed to activate")
: nls.localize('shellIntegration.no', 'No')
);
const detailedAdditions: string[] = [];
if (instance.shellType) {
detailedAdditions.push(`Shell type: \`${instance.shellType}\``);
}
const cwd = instance.cwd;
if (cwd) {
detailedAdditions.push(`Current working directory: \`${cwd}\``);
}
const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences);
if (seenSequences.length > 0) {
detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`);
}
const promptType = instance.capabilities.get(TerminalCapability.PromptTypeDetection)?.promptType;
if (promptType) {
detailedAdditions.push(`Prompt type: \`${promptType}\``);
}
const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString();
if (combinedString !== undefined) {
detailedAdditions.push(`Prompt input: \`\`\`${combinedString}\`\`\``);
}
const detailedAdditionsString = detailedAdditions.length > 0
? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n')
: '';
instance.statusList.add({
id: TerminalStatus.ShellIntegrationInfo,
severity: Severity.Info,
tooltip: `${nls.localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`,
detailedTooltip: `${nls.localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${detailedAdditionsString}`
});
}
async runCommand(commandLine: string, shouldExecute: boolean, commandId?: string): Promise<void> {
let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection);
const siInjectionEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) === true;

View File

@ -74,6 +74,12 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
private _terminalTabsSingleSelectedContextKey: IContextKey<boolean>;
private _isSplitContextKey: IContextKey<boolean>;
private _hasText: boolean = true;
get hasText(): boolean { return this._hasText; }
private _hasActionBar: boolean = true;
get hasActionBar(): boolean { return this._hasActionBar; }
constructor(
container: HTMLElement,
@IContextKeyService contextKeyService: IContextKeyService,
@ -94,7 +100,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
getHeight: () => TerminalTabsListSizes.TabHeight,
getTemplateId: () => 'terminal.tabs'
},
[instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER), () => this.getSelectedElements())],
[instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER), () => this.getSelectedElements(), () => this.hasText, () => this.hasActionBar)],
{
horizontalScrolling: false,
supportDynamicHeights: false,
@ -248,15 +254,29 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
const instance = this.getFocusedElements();
this._isSplitContextKey.set(instance.length > 0 && this._terminalGroupService.instanceIsSplit(instance[0]));
}
override layout(height?: number, width?: number): void {
super.layout(height, width);
const actualWidth = width ?? this.getHTMLElement().clientWidth;
const newHasText = actualWidth >= TerminalTabsListSizes.MidpointViewWidth;
const newHasActionBar = actualWidth > TerminalTabsListSizes.ActionbarMinimumWidth;
if (this._hasText !== newHasText || this._hasActionBar !== newHasActionBar) {
this._hasText = newHasText;
this._hasActionBar = newHasActionBar;
this.refresh();
}
}
}
class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminalTabEntryTemplate> {
templateId = 'terminal.tabs';
constructor(
private readonly _container: HTMLElement,
_container: HTMLElement,
private readonly _labels: ResourceLabels,
private readonly _getSelection: () => ITerminalInstance[],
private readonly _getHasText: () => boolean,
private readonly _getHasActionBar: () => boolean,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ -321,25 +341,9 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal
};
}
shouldHideText(): boolean {
return this._container ? this.getContainerWidthCachedForTask() < TerminalTabsListSizes.MidpointViewWidth : false;
}
shouldHideActionBar(): boolean {
return this._container ? this.getContainerWidthCachedForTask() <= TerminalTabsListSizes.ActionbarMinimumWidth : false;
}
private _cachedContainerWidth = -1;
getContainerWidthCachedForTask(): number {
if (this._cachedContainerWidth === -1) {
this._cachedContainerWidth = this._container.clientWidth;
queueMicrotask(() => this._cachedContainerWidth = -1);
}
return this._cachedContainerWidth;
}
renderElement(instance: ITerminalInstance, index: number, template: ITerminalTabEntryTemplate): void {
const hasText = !this.shouldHideText();
const hasText = this._getHasText();
const hasActionBar = this._getHasActionBar();
const group = this._terminalGroupService.getGroupForInstance(instance);
if (!group) {
@ -365,7 +369,6 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal
template.context.hoverActions = hoverInfo.actions;
const iconId = this._instantiationService.invokeFunction(getIconId, instance);
const hasActionbar = !this.shouldHideActionBar();
let label: string = '';
if (!hasText) {
const primaryStatus = instance.statusList.primary;
@ -385,7 +388,7 @@ class TerminalTabsRenderer implements IListRenderer<ITerminalInstance, ITerminal
}
}
if (!hasActionbar) {
if (!hasActionBar) {
template.actionBar.clear();
}

View File

@ -3,18 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { ITerminalInstance } from './terminal.js';
import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js';
import { asArray } from '../../../../base/common/arrays.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js';
import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { TerminalStatus } from './terminalStatusList.js';
import Severity from '../../../../base/common/severity.js';
import { StorageScope, StorageTarget, type IStorageService } from '../../../../platform/storage/common/storage.js';
import { TerminalStorageKeys } from '../common/terminalStorageKeys.js';
import type { ITerminalStatusHoverAction } from '../common/terminal.js';
import { basename } from '../../../../base/common/path.js';
import { localize } from '../../../../nls.js';
import { StorageScope, StorageTarget, type IStorageService } from '../../../../platform/storage/common/storage.js';
import type { ITerminalStatusHoverAction } from '../common/terminal.js';
import { TerminalStorageKeys } from '../common/terminalStorageKeys.js';
import { ITerminalInstance } from './terminal.js';
export function getInstanceHoverInfo(instance: ITerminalInstance, storageService: IStorageService): { content: MarkdownString; actions: IHoverAction[] } {
const showDetailed = parseInt(storageService.get(TerminalStorageKeys.TabsShowDetailed, StorageScope.APPLICATION) ?? '0');
@ -77,48 +74,3 @@ export function getShellProcessTooltip(instance: ITerminalInstance, showDetailed
return lines.length ? `\n\n---\n\n${lines.join('\n')}` : '';
}
export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) {
if (!instance.xterm) {
return;
}
const cmdDetectionType = (
instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection
? localize('shellIntegration.rich', 'Rich')
: instance.capabilities.has(TerminalCapability.CommandDetection)
? localize('shellIntegration.basic', 'Basic')
: instance.usedShellIntegrationInjection
? localize('shellIntegration.injectionFailed', "Injection failed to activate")
: localize('shellIntegration.no', 'No')
);
const detailedAdditions: string[] = [];
if (instance.shellType) {
detailedAdditions.push(`Shell type: \`${instance.shellType}\``);
}
const cwd = instance.cwd;
if (cwd) {
detailedAdditions.push(`Current working directory: \`${cwd}\``);
}
const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences);
if (seenSequences.length > 0) {
detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`);
}
const promptType = instance.capabilities.get(TerminalCapability.PromptTypeDetection)?.promptType;
if (promptType) {
detailedAdditions.push(`Prompt type: \`${promptType}\``);
}
const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString();
if (combinedString !== undefined) {
detailedAdditions.push(`Prompt input: \`\`\`${combinedString}\`\`\``);
}
const detailedAdditionsString = detailedAdditions.length > 0
? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n')
: '';
instance.statusList.add({
id: TerminalStatus.ShellIntegrationInfo,
severity: Severity.Info,
tooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`,
detailedTooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${detailedAdditionsString}`
});
}