Skip to content

Commit eb87b2e

Browse files
committed
Refactor LanguageClientConsumer to wait properly
This makes a lot more sense and fixes a long-standing bug. Also cleaned up a lot while at it.
1 parent e831373 commit eb87b2e

15 files changed

+266
-392
lines changed

src/features/CodeActions.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@ import vscode = require("vscode");
55
import { ILogger } from "../logging";
66

77
export class CodeActionsFeature implements vscode.Disposable {
8-
private showDocumentationCommand: vscode.Disposable;
8+
private command: vscode.Disposable;
99

1010
constructor(private log: ILogger) {
1111
// NOTE: While not exposed to the user via package.json, this is
1212
// required as the server's code action sends across a command name.
1313
//
1414
// TODO: In the far future with LSP 3.19 the server can just set a URL
1515
// and this can go away. See https://github.com/microsoft/language-server-protocol/issues/1548
16-
this.showDocumentationCommand =
16+
this.command =
1717
vscode.commands.registerCommand("PowerShell.ShowCodeActionDocumentation", async (ruleName: string) => {
1818
await this.showRuleDocumentation(ruleName);
1919
});
2020
}
2121

2222
public dispose(): void {
23-
this.showDocumentationCommand.dispose();
23+
this.command.dispose();
2424
}
2525

26-
public async showRuleDocumentation(ruleId: string): Promise<void> {
26+
private async showRuleDocumentation(ruleId: string): Promise<void> {
2727
const pssaDocBaseURL = "https://docs.microsoft.com/powershell/utility-modules/psscriptanalyzer/rules/";
2828

2929
if (!ruleId) {

src/features/Console.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ export class ConsoleFeature extends LanguageClientConsumer {
200200
} else {
201201
selectionRange = editor.document.lineAt(editor.selection.start.line).range;
202202
}
203-
204-
await this.languageClient?.sendRequest(EvaluateRequestType, {
203+
const client = await LanguageClientConsumer.getLanguageClient();
204+
await client.sendRequest(EvaluateRequestType, {
205205
expression: editor.document.getText(selectionRange),
206206
});
207207

@@ -217,19 +217,19 @@ export class ConsoleFeature extends LanguageClientConsumer {
217217
for (const command of this.commands) {
218218
command.dispose();
219219
}
220+
220221
for (const handler of this.handlers) {
221222
handler.dispose();
222223
}
223224
}
224225

225-
public override setLanguageClient(languageClient: LanguageClient): void {
226-
this.languageClient = languageClient;
226+
public override onLanguageClientSet(languageClient: LanguageClient): void {
227227
this.handlers = [
228-
this.languageClient.onRequest(
228+
languageClient.onRequest(
229229
ShowChoicePromptRequestType,
230230
(promptDetails) => showChoicePrompt(promptDetails)),
231231

232-
this.languageClient.onRequest(
232+
languageClient.onRequest(
233233
ShowInputPromptRequestType,
234234
(promptDetails) => showInputPrompt(promptDetails)),
235235
];

src/features/DebugSession.ts

+79-204
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
126126
private sessionCount = 1;
127127
private tempDebugProcess: PowerShellProcess | undefined;
128128
private tempSessionDetails: IEditorServicesSessionDetails | undefined;
129+
private commands: Disposable[];
129130
private handlers: Disposable[] = [];
130131

131132
constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: ILogger) {
@@ -138,15 +139,34 @@ export class DebugSessionFeature extends LanguageClientConsumer
138139
context.subscriptions.push(debug.registerDebugConfigurationProvider("PowerShell", this, triggerKind));
139140
});
140141
context.subscriptions.push(debug.registerDebugAdapterDescriptorFactory("PowerShell", this));
142+
143+
// NOTE: While process and runspace IDs are numbers, command
144+
// substitutions in VS Code debug configurations are required to return
145+
// strings. Hence to the `toString()` on these.
146+
this.commands = [
147+
commands.registerCommand("PowerShell.PickPSHostProcess", async () => {
148+
149+
const processId = await this.pickPSHostProcess();
150+
return processId?.toString();
151+
}),
152+
commands.registerCommand("PowerShell.PickRunspace", async (processId) => {
153+
const runspace = await this.pickRunspace(processId);
154+
return runspace?.toString();
155+
}, this),
156+
];
141157
}
142158

143159
public dispose(): void {
160+
for (const command of this.commands) {
161+
command.dispose();
162+
}
163+
144164
for (const handler of this.handlers) {
145165
handler.dispose();
146166
}
147167
}
148168

149-
public override setLanguageClient(languageClient: LanguageClient): void {
169+
public override onLanguageClientSet(languageClient: LanguageClient): void {
150170
this.handlers = [
151171
languageClient.onNotification(
152172
StartDebuggerNotificationType,
@@ -477,7 +497,7 @@ export class DebugSessionFeature extends LanguageClientConsumer
477497

478498
// If nothing is set, prompt for the processId.
479499
if (!config.customPipeName && !config.processId) {
480-
config.processId = await commands.executeCommand("PowerShell.PickPSHostProcess");
500+
config.processId = await this.pickPSHostProcess();
481501
// No process selected. Cancel attach.
482502
if (!config.processId) {
483503
return PREVENT_DEBUG_START;
@@ -511,10 +531,66 @@ export class DebugSessionFeature extends LanguageClientConsumer
511531

512532
return config;
513533
}
534+
535+
private async pickPSHostProcess(): Promise<number | undefined> {
536+
// Start with the current PowerShell process in the list.
537+
const items: IProcessItem[] = [];
538+
539+
const client = await LanguageClientConsumer.getLanguageClient();
540+
const response = await client.sendRequest(GetPSHostProcessesRequestType, {});
541+
for (const process of response) {
542+
let windowTitle = "";
543+
if (process.mainWindowTitle) {
544+
windowTitle = `, ${process.mainWindowTitle}`;
545+
}
546+
547+
items.push({
548+
label: process.processName,
549+
description: `PID: ${process.processId.toString()}${windowTitle}`,
550+
processId: process.processId,
551+
});
552+
}
553+
554+
if (items.length === 0) {
555+
return Promise.reject("There are no PowerShell host processes to attach.");
556+
}
557+
558+
const options: QuickPickOptions = {
559+
placeHolder: "Select a PowerShell host process to attach.",
560+
matchOnDescription: true,
561+
matchOnDetail: true,
562+
};
563+
564+
const item = await window.showQuickPick(items, options);
565+
566+
return item?.processId ?? undefined;
567+
}
568+
569+
private async pickRunspace(processId: number): Promise<number | undefined> {
570+
const client = await LanguageClientConsumer.getLanguageClient();
571+
const response = await client.sendRequest(GetRunspaceRequestType, { processId });
572+
const items: IRunspaceItem[] = [];
573+
for (const runspace of response) {
574+
items.push({
575+
label: runspace.name,
576+
description: `ID: ${runspace.id} - ${runspace.availability}`,
577+
id: runspace.id,
578+
});
579+
}
580+
581+
const options: QuickPickOptions = {
582+
placeHolder: "Select PowerShell runspace to debug",
583+
matchOnDescription: true,
584+
matchOnDetail: true,
585+
};
586+
587+
const item = await window.showQuickPick(items, options);
588+
589+
return item?.id ?? undefined;
590+
}
514591
}
515592

516593
export class SpecifyScriptArgsFeature implements Disposable {
517-
518594
private command: Disposable;
519595
private context: ExtensionContext;
520596

@@ -572,111 +648,6 @@ interface IPSHostProcessInfo {
572648
export const GetPSHostProcessesRequestType =
573649
new RequestType<IGetPSHostProcessesArguments, IPSHostProcessInfo[], string>("powerShell/getPSHostProcesses");
574650

575-
export class PickPSHostProcessFeature extends LanguageClientConsumer {
576-
private command: Disposable;
577-
private waitingForClientToken?: CancellationTokenSource;
578-
private getLanguageClientResolve?: (value: LanguageClient) => void;
579-
580-
constructor(private logger: ILogger) {
581-
super();
582-
583-
this.command = commands.registerCommand("PowerShell.PickPSHostProcess", async () => {
584-
return this.pickPSHostProcess();
585-
});
586-
}
587-
588-
public override setLanguageClient(languageClient: LanguageClient): void {
589-
this.languageClient = languageClient;
590-
591-
if (this.waitingForClientToken && this.getLanguageClientResolve) {
592-
this.getLanguageClientResolve(this.languageClient);
593-
this.clearWaitingToken();
594-
}
595-
}
596-
597-
public dispose(): void {
598-
this.command.dispose();
599-
}
600-
601-
private getLanguageClient(): Promise<LanguageClient> {
602-
if (this.languageClient !== undefined) {
603-
return Promise.resolve(this.languageClient);
604-
} else {
605-
// If PowerShell isn't finished loading yet, show a loading message
606-
// until the LanguageClient is passed on to us
607-
this.waitingForClientToken = new CancellationTokenSource();
608-
609-
return new Promise<LanguageClient>(
610-
(resolve, reject) => {
611-
this.getLanguageClientResolve = resolve;
612-
613-
void window
614-
.showQuickPick(
615-
["Cancel"],
616-
{ placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." },
617-
this.waitingForClientToken?.token)
618-
.then((response) => {
619-
if (response === "Cancel") {
620-
this.clearWaitingToken();
621-
reject();
622-
}
623-
}, undefined);
624-
625-
// Cancel the loading prompt after 60 seconds
626-
setTimeout(() => {
627-
if (this.waitingForClientToken) {
628-
this.clearWaitingToken();
629-
reject();
630-
631-
void this.logger.writeAndShowError("Attach to PowerShell host process: PowerShell session took too long to start.");
632-
}
633-
}, 60000);
634-
},
635-
);
636-
}
637-
}
638-
639-
private async pickPSHostProcess(): Promise<number | undefined> {
640-
// We need the language client in order to send the request.
641-
await this.getLanguageClient();
642-
643-
// Start with the current PowerShell process in the list.
644-
const items: IProcessItem[] = [];
645-
646-
const response = await this.languageClient?.sendRequest(GetPSHostProcessesRequestType, {});
647-
for (const process of response ?? []) {
648-
let windowTitle = "";
649-
if (process.mainWindowTitle) {
650-
windowTitle = `, ${process.mainWindowTitle}`;
651-
}
652-
653-
items.push({
654-
label: process.processName,
655-
description: `PID: ${process.processId.toString()}${windowTitle}`,
656-
processId: process.processId,
657-
});
658-
}
659-
660-
if (items.length === 0) {
661-
return Promise.reject("There are no PowerShell host processes to attach to.");
662-
}
663-
664-
const options: QuickPickOptions = {
665-
placeHolder: "Select a PowerShell host process to attach to",
666-
matchOnDescription: true,
667-
matchOnDetail: true,
668-
};
669-
670-
const item = await window.showQuickPick(items, options);
671-
672-
return item?.processId ?? undefined;
673-
}
674-
675-
private clearWaitingToken(): void {
676-
this.waitingForClientToken?.dispose();
677-
this.waitingForClientToken = undefined;
678-
}
679-
}
680651

681652
interface IRunspaceItem extends QuickPickItem {
682653
id: number; // payload for the QuickPick UI
@@ -694,99 +665,3 @@ interface IRunspace {
694665

695666
export const GetRunspaceRequestType =
696667
new RequestType<IGetRunspaceRequestArguments, IRunspace[], string>("powerShell/getRunspace");
697-
698-
export class PickRunspaceFeature extends LanguageClientConsumer {
699-
private command: Disposable;
700-
private waitingForClientToken?: CancellationTokenSource;
701-
private getLanguageClientResolve?: (value: LanguageClient) => void;
702-
703-
constructor(private logger: ILogger) {
704-
super();
705-
706-
this.command = commands.registerCommand("PowerShell.PickRunspace",
707-
async (processId) => { return this.pickRunspace(processId); },
708-
this);
709-
}
710-
711-
public override setLanguageClient(languageClient: LanguageClient): void {
712-
this.languageClient = languageClient;
713-
714-
if (this.waitingForClientToken && this.getLanguageClientResolve) {
715-
this.getLanguageClientResolve(this.languageClient);
716-
this.clearWaitingToken();
717-
}
718-
}
719-
720-
public dispose(): void {
721-
this.command.dispose();
722-
}
723-
724-
private getLanguageClient(): Promise<LanguageClient> {
725-
if (this.languageClient) {
726-
return Promise.resolve(this.languageClient);
727-
} else {
728-
// If PowerShell isn't finished loading yet, show a loading message
729-
// until the LanguageClient is passed on to us
730-
this.waitingForClientToken = new CancellationTokenSource();
731-
732-
return new Promise<LanguageClient>(
733-
(resolve, reject) => {
734-
this.getLanguageClientResolve = resolve;
735-
736-
void window
737-
.showQuickPick(
738-
["Cancel"],
739-
{ placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." },
740-
this.waitingForClientToken?.token)
741-
.then((response) => {
742-
if (response === "Cancel") {
743-
this.clearWaitingToken();
744-
reject();
745-
}
746-
}, undefined);
747-
748-
// Cancel the loading prompt after 60 seconds
749-
setTimeout(() => {
750-
if (this.waitingForClientToken) {
751-
this.clearWaitingToken();
752-
reject();
753-
754-
void this.logger.writeAndShowError(
755-
"Attach to PowerShell host process: PowerShell session took too long to start.");
756-
}
757-
}, 60000);
758-
},
759-
);
760-
}
761-
}
762-
763-
private async pickRunspace(processId: number): Promise<number | undefined> {
764-
// We need the language client in order to send the request.
765-
await this.getLanguageClient();
766-
767-
const response = await this.languageClient?.sendRequest(GetRunspaceRequestType, { processId });
768-
const items: IRunspaceItem[] = [];
769-
for (const runspace of response ?? []) {
770-
items.push({
771-
label: runspace.name,
772-
description: `ID: ${runspace.id} - ${runspace.availability}`,
773-
id: runspace.id,
774-
});
775-
}
776-
777-
const options: QuickPickOptions = {
778-
placeHolder: "Select PowerShell runspace to debug",
779-
matchOnDescription: true,
780-
matchOnDetail: true,
781-
};
782-
783-
const item = await window.showQuickPick(items, options);
784-
785-
return item?.id ?? undefined;
786-
}
787-
788-
private clearWaitingToken(): void {
789-
this.waitingForClientToken?.dispose();
790-
this.waitingForClientToken = undefined;
791-
}
792-
}

0 commit comments

Comments
 (0)