diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index c5951189a8000..f113bb4360be7 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -623,6 +623,9 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ 'workbench.action.togglePanel', 'workbench.action.quickOpenView', 'workbench.action.toggleMaximizedPanel', + 'workbench.action.zoomIn', + 'workbench.action.zoomOut', + 'workbench.action.zoomReset', 'notification.acceptPrimaryAction', 'runCommands', 'workbench.action.terminal.chat.start', diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index f614bdea09cf1..834904a62033c 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -14,6 +14,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ResultKind } from '../../../../../platform/keybinding/common/keybindingResolver.js'; import { TerminalCapability, type ICwdDetectionCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; import { GeneralShellType, ITerminalChildProcess, ITerminalProfile, TerminalLocation, TitleEventSource, type IShellLaunchConfig, type ITerminalBackend, type ITerminalProcessOptions } from '../../../../../platform/terminal/common/terminal.js'; @@ -27,7 +28,7 @@ import { TerminalConfigurationService } from '../../browser/terminalConfiguratio import { parseExitResult, TerminalInstance, TerminalInstanceColorProvider, TerminalLabelComputer } from '../../browser/terminalInstance.js'; import { IEnvironmentVariableService } from '../../common/environmentVariable.js'; import { EnvironmentVariableService } from '../../common/environmentVariableService.js'; -import { ITerminalProfileResolverService, ProcessState } from '../../common/terminal.js'; +import { ITerminalProfileResolverService, ProcessState, DEFAULT_COMMANDS_TO_SKIP_SHELL } from '../../common/terminal.js'; import { TERMINAL_BACKGROUND_COLOR } from '../../common/terminalColorRegistry.js'; import { TestViewDescriptorService } from './xterm/xtermTerminal.test.js'; import { fixPath } from '../../../../services/search/test/browser/queryBuilder.test.js'; @@ -128,7 +129,8 @@ suite('Workbench - TerminalInstance', () => { suite('TerminalInstance', () => { let terminalInstance: ITerminalInstance; - test('should create an instance of TerminalInstance with env from default profile', async () => { + + async function createTerminalInstance(): Promise { const instantiationService = workbenchInstantiationService({ configurationService: () => new TestConfigurationService({ files: {}, @@ -152,8 +154,14 @@ suite('Workbench - TerminalInstance', () => { instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService))); instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService())); instantiationService.stub(ITerminalService, { setNextCommandId: async () => { } } as Partial); - terminalInstance = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {})); - // //Wait for the teminalInstance._xtermReadyPromise to resolve + const instance = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {})); + await instance.xtermReadyPromise; + return instance; + } + + test('should create an instance of TerminalInstance with env from default profile', async () => { + terminalInstance = await createTerminalInstance(); + // Wait for the terminal instance to resolve shell launch config env. await new Promise(resolve => setTimeout(resolve, 100)); deepStrictEqual(terminalInstance.shellLaunchConfig.env, { TEST: 'TEST' }); }); @@ -198,6 +206,67 @@ suite('Workbench - TerminalInstance', () => { // Verify that the task name is preserved strictEqual(taskTerminal.title, 'Test Task Name', 'Task terminal should preserve API-set title'); }); + + test('custom key event handler should handle commands in DEFAULT_COMMANDS_TO_SKIP_SHELL in VS Code and not xterm when sendKeybindingsToShell is disabled', async () => { + const instance = await createTerminalInstance(); + const keybindingService = instance['_keybindingService']; + const originalSoftDispatch = keybindingService.softDispatch; + keybindingService.softDispatch = () => ({ kind: ResultKind.KbFound, commandId: 'workbench.action.zoomIn', commandArgs: undefined, isBubble: false }); + + let capturedHandler: ((e: KeyboardEvent) => boolean) | undefined; + instance.xterm!.raw.attachCustomKeyEventHandler = handler => { capturedHandler = handler; }; + const container = document.createElement('div'); + document.body.appendChild(container); + instance.attachToElement(container); + instance.setVisible(true); + + const event = new KeyboardEvent('keydown', { key: '=', cancelable: true }); + try { + deepStrictEqual( + { result: capturedHandler?.(event), defaultPrevented: event.defaultPrevented }, + { result: false, defaultPrevented: true } + ); + } finally { + keybindingService.softDispatch = originalSoftDispatch; + container.remove(); + } + }); + + test('custom key event handler should intercept Meta-modified keys that resolve to a command when sendKeybindingsToShell is disabled', async () => { + const instance = await createTerminalInstance(); + const keybindingService = instance['_keybindingService']; + const originalSoftDispatch = keybindingService.softDispatch; + strictEqual(DEFAULT_COMMANDS_TO_SKIP_SHELL.includes('test.metaKeyInterceptCommand'), false); + keybindingService.softDispatch = () => ({ kind: ResultKind.KbFound, commandId: 'test.metaKeyInterceptCommand', commandArgs: undefined, isBubble: false }); + + let capturedHandler: ((e: KeyboardEvent) => boolean) | undefined; + instance.xterm!.raw.attachCustomKeyEventHandler = handler => { capturedHandler = handler; }; + const container = document.createElement('div'); + document.body.appendChild(container); + instance.attachToElement(container); + instance.setVisible(true); + + const event = new KeyboardEvent('keydown', { key: '=', metaKey: true, cancelable: true }); + try { + deepStrictEqual( + { result: capturedHandler?.(event), defaultPrevented: event.defaultPrevented }, + { result: false, defaultPrevented: true } + ); + } finally { + keybindingService.softDispatch = originalSoftDispatch; + container.remove(); + } + }); + }); + suite('DEFAULT_COMMANDS_TO_SKIP_SHELL', () => { + test('should include zoom commands so they are not consumed by kitty keyboard protocol', () => { + deepStrictEqual( + ['workbench.action.zoomIn', 'workbench.action.zoomOut', 'workbench.action.zoomReset'].every( + cmd => DEFAULT_COMMANDS_TO_SKIP_SHELL.includes(cmd) + ), + true + ); + }); }); suite('parseExitResult', () => { test('should return no message for exit code = undefined', () => {