diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 0b1215a143fbe..a43271a5e609a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -28,6 +28,8 @@ import { SuggestAddon } from './terminalSuggestAddon.js'; import { TerminalClipboardContribution } from '../../clipboard/browser/terminal.clipboard.contribution.js'; import { PwshCompletionProviderAddon } from './pwshCompletionProviderAddon.js'; import { SimpleSuggestContext } from '../../../../services/suggest/browser/simpleSuggestWidget.js'; +import { SuggestDetailsClassName } from '../../../../services/suggest/browser/simpleSuggestWidgetDetails.js'; +import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; registerSingleton(ITerminalCompletionService, TerminalCompletionService, InstantiationType.Delayed); @@ -153,7 +155,16 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!); } addon.setScreen(xterm.element!.querySelector('.xterm-screen')!); - this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget())); + + this.add(dom.addDisposableListener(this._ctx.instance.domElement, dom.EventType.FOCUS_OUT, (e) => { + const focusedElement = e.relatedTarget as HTMLElement; + if (focusedElement.className === SuggestDetailsClassName) { + // Don't hide the suggest widget if the focus is moving to the details + return; + } + addon.hideSuggestWidget(); + })); + this.add(addon.onAcceptedCompletion(async text => { this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); @@ -263,11 +274,25 @@ registerActiveInstanceAction({ run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleExplainMode() }); +registerActiveInstanceAction({ + id: TerminalSuggestCommandId.ToggleDetailsFocus, + title: localize2('workbench.action.terminal.suggestToggleDetailsFocus', 'Suggest Toggle Suggestion Focus'), + f1: false, + // HACK: This does not work with a precondition of `TerminalContextKeys.suggestWidgetVisible`, so make sure to not override the editor's keybinding + precondition: EditorContextKeys.textInputFocus.negate(), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Space, + mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Space } + }, + run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.toggleSuggestionFocus() +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.ToggleDetails, title: localize2('workbench.action.terminal.suggestToggleDetails', 'Suggest Toggle Details'), f1: false, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.isOpen, TerminalContextKeys.focus, TerminalContextKeys.suggestWidgetVisible, SimpleSuggestContext.HasFocusedSuggestion), keybinding: { // HACK: Force weight to be higher than that to start terminal chat weight: KeybindingWeight.ExternalExtension + 2, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 6ed3e16864e1d..c0cb3ab00f169 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -230,6 +230,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._suggestWidget?.toggleExplainMode(); } + toggleSuggestionFocus(): void { + this._suggestWidget?.toggleDetailsFocus(); + } + toggleSuggestionDetails(): void { this._suggestWidget?.toggleDetails(); } @@ -373,6 +377,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const suggestWidget = this._ensureSuggestWidget(this._terminal); suggestWidget.setCompletionModel(model); + this._register(suggestWidget.onDidFocus(() => this._terminal?.focus())); if (!this._promptInputModel || !explicitlyInvoked && model.items.length === 0) { return; } @@ -413,8 +418,29 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest listInactiveFocusOutline: activeContrastBorder })); this._register(this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e))); - this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false))); + this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.reset())); this._register(this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true))); + + const element = this._terminal?.element?.querySelector('.xterm-helper-textarea'); + if (element) { + this._register(dom.addDisposableListener(dom.getActiveDocument(), 'click', (event) => { + const target = event.target as HTMLElement; + if (this._terminal?.element?.contains(target)) { + this._suggestWidget?.hide(); + } + })); + } + + this._register(this._suggestWidget.onDidBlurDetails((e) => { + const elt = e.relatedTarget as HTMLElement; + if (this._terminal?.element?.contains(elt)) { + // Do nothing, just the terminal getting focused + // If there was a mouse click, the suggest widget will be + // hidden above + return; + } + this._suggestWidget?.hide(); + })); this._terminalSuggestWidgetVisibleContextKey.set(false); } return this._suggestWidget; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index bff806680b424..049ce2b297682 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -14,7 +14,8 @@ export const enum TerminalSuggestCommandId { ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', - ToggleDetails = 'workbench.action.terminal.suggestToggleDetails' + ToggleDetails = 'workbench.action.terminal.suggestToggleDetails', + ToggleDetailsFocus = 'workbench.action.terminal.suggestToggleDetailsFocus', } export const defaultTerminalSuggestCommandsToSkipShell = [ @@ -27,5 +28,6 @@ export const defaultTerminalSuggestCommandsToSkipShell = [ TerminalSuggestCommandId.HideSuggestWidget, TerminalSuggestCommandId.ClearSuggestCache, TerminalSuggestCommandId.RequestCompletions, - TerminalSuggestCommandId.ToggleDetails + TerminalSuggestCommandId.ToggleDetails, + TerminalSuggestCommandId.ToggleDetailsFocus, ]; diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts index f397073a7b860..0c925bfd2b303 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidget.ts @@ -98,6 +98,8 @@ export class SimpleSuggestWidget extends Disposable { readonly onDidShow: Event = this._onDidShow.event; private readonly _onDidFocus = new PauseableEmitter(); readonly onDidFocus: Event = this._onDidFocus.event; + private readonly _onDidBlurDetails = this._register(new Emitter()); + readonly onDidBlurDetails = this._onDidBlurDetails.event; get list(): List { return this._list; } @@ -223,6 +225,7 @@ export class SimpleSuggestWidget extends Disposable { const details: SimpleSuggestDetailsWidget = this._register(instantiationService.createInstance(SimpleSuggestDetailsWidget)); this._register(details.onDidClose(() => this.toggleDetails())); this._details = this._register(new SimpleSuggestDetailsOverlay(details, this._listElement)); + this._register(dom.addDisposableListener(this._details.widget.domNode, 'blur', (e) => this._onDidBlurDetails.fire(e))); if (options.statusBarMenuId) { this._status = this._register(instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, options.statusBarMenuId)); diff --git a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts index 595afceb04300..0281697454979 100644 --- a/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts +++ b/src/vs/workbench/services/suggest/browser/simpleSuggestWidgetDetails.ts @@ -21,6 +21,8 @@ export function canExpandCompletionItem(item: SimpleCompletionItem | undefined): return !!item && Boolean(item.completion.detail && item.completion.detail !== item.completion.label); } +export const SuggestDetailsClassName = 'suggest-details'; + export class SimpleSuggestDetailsWidget { readonly domNode: HTMLDivElement; @@ -158,7 +160,7 @@ export class SimpleSuggestDetailsWidget { this._renderDisposeable.add(renderedContents); } - // this.domNode.classList.toggle('detail-and-doc', !!documentation); + this.domNode.classList.toggle('detail-and-doc', !!detail && !!documentation); this.domNode.style.userSelect = 'text'; this.domNode.tabIndex = -1;