Skip to content

Commit 15da193

Browse files
authored
Allow completions to be requested earlier (microsoft#236630)
1 parent eee5e76 commit 15da193

File tree

3 files changed

+107
-52
lines changed

3 files changed

+107
-52
lines changed

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
504504
// Resolve the executable ahead of time if shell integration is enabled, this should not
505505
// be done for custom PTYs as that would cause extension Pseudoterminal-based terminals
506506
// to hang in resolver extensions
507+
let os: OperatingSystem | undefined;
507508
if (!this.shellLaunchConfig.customPtyImplementation && this._terminalConfigurationService.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) {
508-
const os = await this._processManager.getBackendOS();
509+
os = await this._processManager.getBackendOS();
509510
const defaultProfile = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os }));
510511
this.shellLaunchConfig.executable = defaultProfile.path;
511512
this.shellLaunchConfig.args = defaultProfile.args;
@@ -521,6 +522,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
521522
}
522523
}
523524

525+
// Resolve the shell type ahead of time to allow features that depend upon it to work
526+
// before the process is actually created (like terminal suggest manual request)
527+
if (os && this.shellLaunchConfig.executable) {
528+
this.setShellType(guessShellTypeFromExecutable(os, this.shellLaunchConfig.executable));
529+
}
530+
524531
await this._createProcess();
525532

526533
// Re-establish the title after reconnect
@@ -2656,3 +2663,46 @@ export class TerminalInstanceColorProvider implements IXtermColorProvider {
26562663
return theme.getColor(SIDE_BAR_BACKGROUND);
26572664
}
26582665
}
2666+
2667+
function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): TerminalShellType | undefined {
2668+
const exeBasename = path.basename(executable);
2669+
const generalShellTypeMap: Map<TerminalShellType, RegExp> = new Map([
2670+
[GeneralShellType.Julia, /^julia$/],
2671+
[GeneralShellType.NuShell, /^nu$/],
2672+
[GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/],
2673+
[GeneralShellType.Python, /^py(?:thon)?$/]
2674+
]);
2675+
for (const [shellType, pattern] of generalShellTypeMap) {
2676+
if (exeBasename.match(pattern)) {
2677+
return shellType;
2678+
}
2679+
}
2680+
2681+
if (os === OperatingSystem.Windows) {
2682+
const windowsShellTypeMap: Map<TerminalShellType, RegExp> = new Map([
2683+
[WindowsShellType.CommandPrompt, /^cmd$/],
2684+
[WindowsShellType.GitBash, /^bash$/],
2685+
[WindowsShellType.Wsl, /^wsl$/]
2686+
]);
2687+
for (const [shellType, pattern] of windowsShellTypeMap) {
2688+
if (exeBasename.match(pattern)) {
2689+
return shellType;
2690+
}
2691+
}
2692+
} else {
2693+
const posixShellTypes: PosixShellType[] = [
2694+
PosixShellType.Bash,
2695+
PosixShellType.Csh,
2696+
PosixShellType.Fish,
2697+
PosixShellType.Ksh,
2698+
PosixShellType.Sh,
2699+
PosixShellType.Zsh,
2700+
];
2701+
for (const type of posixShellTypes) {
2702+
if (exeBasename === type) {
2703+
return type;
2704+
}
2705+
}
2706+
}
2707+
return undefined;
2708+
}

src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts

Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@ import { DisposableStore, MutableDisposable, toDisposable } from '../../../../..
1212
import { isWindows } from '../../../../../base/common/platform.js';
1313
import { localize2 } from '../../../../../nls.js';
1414
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
15-
import { ContextKeyExpr, IContextKey, IContextKeyService, IReadableSet } from '../../../../../platform/contextkey/common/contextkey.js';
15+
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
1616
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1717
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
18-
import { GeneralShellType, TerminalLocation, TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js';
18+
import { GeneralShellType, TerminalLocation } from '../../../../../platform/terminal/common/terminal.js';
1919
import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js';
2020
import { registerActiveInstanceAction } from '../../../terminal/browser/terminalActions.js';
2121
import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js';
22-
import { TERMINAL_CONFIG_SECTION, type ITerminalConfiguration } from '../../../terminal/common/terminal.js';
2322
import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js';
2423
import { TerminalSuggestCommandId } from '../common/terminal.suggest.js';
2524
import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js';
@@ -42,8 +41,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
4241

4342
private readonly _addon: MutableDisposable<SuggestAddon> = new MutableDisposable();
4443
private readonly _pwshAddon: MutableDisposable<PwshCompletionProviderAddon> = new MutableDisposable();
45-
private _terminalSuggestWidgetContextKeys: IReadableSet<string> = new Set(TerminalContextKeys.suggestWidgetVisible.key);
46-
private _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>;
44+
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>;
4745

4846
get addon(): SuggestAddon | undefined { return this._addon.value; }
4947
get pwshAddon(): PwshCompletionProviderAddon | undefined { return this._pwshAddon.value; }
@@ -87,23 +85,15 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
8785
if (!enabled) {
8886
return;
8987
}
88+
this._loadAddons(xterm.raw);
9089
this.add(Event.runAndSubscribe(this._ctx.instance.onDidChangeShellType, async () => {
91-
this._loadAddons(xterm.raw);
92-
}));
93-
this.add(this._contextKeyService.onDidChangeContext(e => {
94-
if (e.affectsSome(this._terminalSuggestWidgetContextKeys)) {
95-
this._loadAddons(xterm.raw);
96-
}
97-
}));
98-
this.add(this._configurationService.onDidChangeConfiguration(e => {
99-
if (e.affectsConfiguration(TerminalSettingId.SendKeybindingsToShell)) {
100-
this._loadAddons(xterm.raw);
101-
}
90+
this._refreshAddons();
10291
}));
10392
}
10493

10594
private _loadPwshCompletionAddon(xterm: RawXtermTerminal): void {
10695
if (this._ctx.instance.shellType !== GeneralShellType.PowerShell) {
96+
this._pwshAddon.clear();
10797
return;
10898
}
10999
const pwshCompletionProviderAddon = this._pwshAddon.value = this._instantiationService.createInstance(PwshCompletionProviderAddon, undefined, this._ctx.instance.capabilities);
@@ -139,42 +129,47 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
139129
}
140130

141131
private _loadAddons(xterm: RawXtermTerminal): void {
142-
const sendingKeybindingsToShell = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).sendKeybindingsToShell;
143-
if (sendingKeybindingsToShell || !this._ctx.instance.shellType) {
144-
this._addon.clear();
145-
this._pwshAddon.clear();
132+
// Don't re-create the addon
133+
if (this._addon.value) {
146134
return;
147135
}
148-
if (this._terminalSuggestWidgetVisibleContextKey) {
149-
const addon = this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._ctx.instance.shellType, this._ctx.instance.capabilities, this._terminalSuggestWidgetVisibleContextKey);
150-
xterm.loadAddon(addon);
151-
this._loadPwshCompletionAddon(xterm);
152-
if (this._ctx.instance.target === TerminalLocation.Editor) {
153-
addon.setContainerWithOverflow(xterm.element!);
154-
} else {
155-
addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!);
156-
}
157-
addon.setScreen(xterm.element!.querySelector('.xterm-screen')!);
158-
this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget()));
159-
this.add(addon.onAcceptedCompletion(async text => {
160-
this._ctx.instance.focus();
161-
this._ctx.instance.sendText(text, false);
162-
}));
163-
const clipboardContrib = TerminalClipboardContribution.get(this._ctx.instance)!;
164-
this.add(clipboardContrib.onWillPaste(() => addon.isPasting = true));
165-
this.add(clipboardContrib.onDidPaste(() => {
166-
// Delay this slightly as synchronizing the prompt input is debounced
167-
setTimeout(() => addon.isPasting = false, 100);
136+
137+
const addon = this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._ctx.instance.shellType, this._ctx.instance.capabilities, this._terminalSuggestWidgetVisibleContextKey);
138+
xterm.loadAddon(addon);
139+
this._loadPwshCompletionAddon(xterm);
140+
if (this._ctx.instance.target === TerminalLocation.Editor) {
141+
addon.setContainerWithOverflow(xterm.element!);
142+
} else {
143+
addon.setContainerWithOverflow(dom.findParentWithClass(xterm.element!, 'panel')!);
144+
}
145+
addon.setScreen(xterm.element!.querySelector('.xterm-screen')!);
146+
this.add(this._ctx.instance.onDidBlur(() => addon.hideSuggestWidget()));
147+
this.add(addon.onAcceptedCompletion(async text => {
148+
this._ctx.instance.focus();
149+
this._ctx.instance.sendText(text, false);
150+
}));
151+
const clipboardContrib = TerminalClipboardContribution.get(this._ctx.instance)!;
152+
this.add(clipboardContrib.onWillPaste(() => addon.isPasting = true));
153+
this.add(clipboardContrib.onDidPaste(() => {
154+
// Delay this slightly as synchronizing the prompt input is debounced
155+
setTimeout(() => addon.isPasting = false, 100);
156+
}));
157+
if (!isWindows) {
158+
let barrier: AutoOpenBarrier | undefined;
159+
this.add(addon.onDidReceiveCompletions(() => {
160+
barrier?.open();
161+
barrier = undefined;
168162
}));
169-
if (!isWindows) {
170-
let barrier: AutoOpenBarrier | undefined;
171-
this.add(addon.onDidReceiveCompletions(() => {
172-
barrier?.open();
173-
barrier = undefined;
174-
}));
175-
}
176163
}
177164
}
165+
166+
private _refreshAddons(): void {
167+
const addon = this._addon.value;
168+
if (!addon) {
169+
return;
170+
}
171+
addon.shellType = this._ctx.instance.shellType;
172+
}
178173
}
179174

180175
registerTerminalContribution(TerminalSuggestContribution.ID, TerminalSuggestContribution);
@@ -190,7 +185,7 @@ registerActiveInstanceAction({
190185
primary: KeyMod.CtrlCmd | KeyCode.Space,
191186
mac: { primary: KeyMod.WinCtrl | KeyCode.Space },
192187
weight: KeybindingWeight.WorkbenchContrib + 1,
193-
when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.terminalShellIntegrationEnabled, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true))
188+
when: ContextKeyExpr.and(TerminalContextKeys.focus, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.Enabled}`, true))
194189
},
195190
run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.requestCompletions(true)
196191
});

src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
7373
private _cancellationTokenSource: CancellationTokenSource | undefined;
7474

7575
isPasting: boolean = false;
76+
shellType: TerminalShellType | undefined;
7677

7778
private readonly _onBell = this._register(new Emitter<void>());
7879
readonly onBell = this._onBell.event;
@@ -89,8 +90,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
8990
[TerminalCompletionItemKind.Argument, Codicon.symbolVariable]
9091
]);
9192

93+
private _shouldSyncWhenReady: boolean = false;
94+
9295
constructor(
93-
private readonly _shellType: TerminalShellType | undefined,
96+
shellType: TerminalShellType | undefined,
9497
private readonly _capabilities: ITerminalCapabilityStore,
9598
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>,
9699
@ITerminalCompletionService private readonly _terminalCompletionService: ITerminalCompletionService,
@@ -101,6 +104,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
101104
) {
102105
super();
103106

107+
this.shellType = shellType;
108+
104109
this._register(Event.runAndSubscribe(Event.any(
105110
this._capabilities.onDidAddCapabilityType,
106111
this._capabilities.onDidRemoveCapabilityType
@@ -113,6 +118,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
113118
this._promptInputModel.onDidChangeInput(e => this._sync(e)),
114119
this._promptInputModel.onDidFinishInput(() => this.hideSuggestWidget()),
115120
);
121+
if (this._shouldSyncWhenReady) {
122+
this._sync(this._promptInputModel);
123+
this._shouldSyncWhenReady = false;
124+
}
116125
}
117126
this._register(commandDetection.onCommandExecuted(() => this.hideSuggestWidget()));
118127
} else {
@@ -140,7 +149,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
140149
return;
141150
}
142151

143-
if (!this._shellType) {
152+
if (!this.shellType) {
144153
return;
145154
}
146155

@@ -156,7 +165,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
156165
await this._extensionService.activateByEvent('onTerminalCompletionsRequested');
157166
}
158167

159-
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this._shellType, token, doNotRequestExtensionCompletions);
168+
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.prefix, this._promptInputModel.cursorIndex, this.shellType, token, doNotRequestExtensionCompletions);
160169
if (!providedCompletions?.length || token.isCancellationRequested) {
161170
return;
162171
}
@@ -237,6 +246,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
237246

238247
async requestCompletions(explicitlyInvoked?: boolean): Promise<void> {
239248
if (!this._promptInputModel) {
249+
this._shouldSyncWhenReady = true;
240250
return;
241251
}
242252

0 commit comments

Comments
 (0)