diff --git a/src/controls/animatedStatusBar.ts b/src/controls/animatedStatusBar.ts deleted file mode 100644 index 1d79612218..0000000000 --- a/src/controls/animatedStatusBar.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { - Disposable, - StatusBarAlignment, - StatusBarItem, - ThemeColor, - window, - Command, - AccessibilityInformation} from "vscode"; - -export function showAnimatedStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable { - const animatedStatusBarItem: AnimatedStatusBarItem = new AnimatedStatusBarItem(text); - animatedStatusBarItem.show(hideWhenDone); - return animatedStatusBarItem; -} - -class AnimatedStatusBarItem implements StatusBarItem { - private readonly animationRate: number; - private statusBarItem: StatusBarItem; - private maxCount: number; - private counter: number; - private baseText: string; - private timerInterval: number; - private elapsedTime: number; - private intervalId: NodeJS.Timer; - private suffixStates: string[]; - - public get id(): string { - return this.statusBarItem.id; - } - - public get name(): string { - return this.statusBarItem.name; - } - - public get accessibilityInformation(): AccessibilityInformation { - return this.statusBarItem.accessibilityInformation; - } - - public get alignment(): StatusBarAlignment { - return this.statusBarItem.alignment; - } - - public get priority(): number { - return this.statusBarItem.priority; - } - - public get text(): string { - return this.statusBarItem.text; - } - - public set text(value: string) { - this.statusBarItem.text = value; - } - - public get tooltip(): string { - return this.statusBarItem.tooltip.toString(); - } - - public set tooltip(value: string) { - this.statusBarItem.tooltip = value; - } - - public get color(): string | ThemeColor { - return this.statusBarItem.color; - } - - public set color(value: string | ThemeColor) { - this.statusBarItem.color = value; - } - - public get backgroundColor(): string | ThemeColor { - return this.statusBarItem.backgroundColor; - } - - public set backgroundColor(value: string | ThemeColor) { - this.statusBarItem.backgroundColor = value; - } - - public get command(): string | Command { - return this.statusBarItem.command; - } - - public set command(value: string | Command) { - this.statusBarItem.command = value; - } - - constructor(baseText: string, alignment?: StatusBarAlignment, priority?: number) { - this.animationRate = 1; - this.statusBarItem = window.createStatusBarItem(alignment, priority); - this.baseText = baseText; - this.counter = 0; - this.suffixStates = [" ", ". ", ".. ", "..."]; - this.maxCount = this.suffixStates.length; - this.timerInterval = ((1 / this.maxCount) * 1000) / this.animationRate; - this.elapsedTime = 0; - } - - public show(hideWhenDone?: Thenable): void { - this.statusBarItem.show(); - this.start(); - if (hideWhenDone !== undefined) { - hideWhenDone.then(() => this.hide()); - } - } - - public hide(): void { - this.stop(); - this.statusBarItem.hide(); - } - - public dispose(): void { - this.statusBarItem.dispose(); - } - - private updateCounter(): void { - this.counter = (this.counter + 1) % this.maxCount; - this.elapsedTime = this.elapsedTime + this.timerInterval; - } - - private updateText(): void { - this.text = this.baseText + this.suffixStates[this.counter]; - } - - private update(): void { - this.updateCounter(); - this.updateText(); - } - - private reset(): void { - this.counter = 0; - this.updateText(); - } - - private start(): void { - this.reset(); - this.intervalId = setInterval(() => this.update(), this.timerInterval); - } - - private stop(): void { - clearInterval(this.intervalId); - } -} diff --git a/src/controls/checkboxQuickPick.ts b/src/controls/checkboxQuickPick.ts index 64c70f0c39..15ca300262 100644 --- a/src/controls/checkboxQuickPick.ts +++ b/src/controls/checkboxQuickPick.ts @@ -18,23 +18,17 @@ export interface ICheckboxQuickPickOptions { confirmPlaceHolder: string; } -const defaultOptions: ICheckboxQuickPickOptions = { confirmPlaceHolder: defaultPlaceHolder}; +const defaultOptions: ICheckboxQuickPickOptions = { confirmPlaceHolder: defaultPlaceHolder }; -export function showCheckboxQuickPick( +export async function showCheckboxQuickPick( items: ICheckboxQuickPickItem[], - options: ICheckboxQuickPickOptions = defaultOptions): Thenable { - - return showInner(items, options).then( - (selectedItem) => { - // We're mutating the original item list so just return it for now. - // If 'selectedItem' is undefined it means the user cancelled the - // inner showQuickPick UI so pass the undefined along. - return selectedItem !== undefined ? items : undefined; - }); + options: ICheckboxQuickPickOptions = defaultOptions): Promise { + + const selectedItem = await showInner(items, options); + return selectedItem !== undefined ? items : undefined; } function getQuickPickItems(items: ICheckboxQuickPickItem[]): vscode.QuickPickItem[] { - const quickPickItems: vscode.QuickPickItem[] = []; quickPickItems.push({ label: confirmItemLabel, description: "" }); @@ -48,40 +42,35 @@ function getQuickPickItems(items: ICheckboxQuickPickItem[]): vscode.QuickPickIte return quickPickItems; } -function showInner( +async function showInner( items: ICheckboxQuickPickItem[], - options: ICheckboxQuickPickOptions): Thenable { - - const quickPickThenable: Thenable = - vscode.window.showQuickPick( - getQuickPickItems(items), - { - ignoreFocusOut: true, - matchOnDescription: true, - placeHolder: options.confirmPlaceHolder, - }); - - return quickPickThenable.then( - (selection) => { - if (!selection) { - return Promise.resolve(undefined); - } - - if (selection.label === confirmItemLabel) { - return selection; - } - - const index: number = getItemIndex(items, selection.label); - - if (index >= 0) { - toggleSelection(items[index]); - } else { - // tslint:disable-next-line:no-console - console.log(`Couldn't find CheckboxQuickPickItem for label '${selection.label}'`); - } - - return showInner(items, options); + options: ICheckboxQuickPickOptions): Promise { + + const selection = await vscode.window.showQuickPick( + getQuickPickItems(items), + { + ignoreFocusOut: true, + matchOnDescription: true, + placeHolder: options.confirmPlaceHolder, }); + + if (selection === undefined) { + return undefined; + } + + if (selection.label === confirmItemLabel) { + return selection; + } + + const index: number = getItemIndex(items, selection.label); + if (index >= 0) { + toggleSelection(items[index]); + } else { + // tslint:disable-next-line:no-console + console.log(`Couldn't find CheckboxQuickPickItem for label '${selection.label}'`); + } + + return showInner(items, options); } function getItemIndex(items: ICheckboxQuickPickItem[], itemLabel: string): number { diff --git a/src/features/CodeActions.ts b/src/features/CodeActions.ts index c1cc7646e1..1d1f7907aa 100644 --- a/src/features/CodeActions.ts +++ b/src/features/CodeActions.ts @@ -11,7 +11,7 @@ export class CodeActionsFeature implements vscode.Disposable { constructor(private log: ILogger) { this.applyEditsCommand = vscode.commands.registerCommand("PowerShell.ApplyCodeActionEdits", (edit: any) => { - Window.activeTextEditor.edit((editBuilder) => { + Window.activeTextEditor?.edit((editBuilder) => { editBuilder.replace( new vscode.Range( edit.StartLineNumber - 1, diff --git a/src/features/Console.ts b/src/features/Console.ts index e1ad971813..b7eacc9d50 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import vscode = require("vscode"); import { NotificationType, RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; @@ -50,19 +48,17 @@ interface IShowChoicePromptRequestArgs { } interface IShowChoicePromptResponseBody { - responseText: string; + responseText: string | undefined; promptCancelled: boolean; } interface IShowInputPromptResponseBody { - responseText: string; + responseText: string | undefined; promptCancelled: boolean; } -function showChoicePrompt( - promptDetails: IShowChoicePromptRequestArgs, - client: LanguageClient): Thenable { +function showChoicePrompt(promptDetails: IShowChoicePromptRequestArgs): Thenable { let resultThenable: Thenable; @@ -121,11 +117,12 @@ function showChoicePrompt( return resultThenable; } -function showInputPrompt(promptDetails: IShowInputPromptRequestArgs): Thenable { - return vscode.window.showInputBox({ placeHolder: promptDetails.name + ": " }).then(onInputEntered); +async function showInputPrompt(promptDetails: IShowInputPromptRequestArgs): Promise { + const responseText = await vscode.window.showInputBox({ placeHolder: promptDetails.name + ": " }); + return onInputEntered(responseText); } -function onItemsSelected(chosenItems: ICheckboxQuickPickItem[]): IShowChoicePromptResponseBody { +function onItemsSelected(chosenItems: ICheckboxQuickPickItem[] | undefined): IShowChoicePromptResponseBody { if (chosenItems !== undefined) { return { promptCancelled: false, @@ -140,7 +137,7 @@ function onItemsSelected(chosenItems: ICheckboxQuickPickItem[]): IShowChoiceProm } } -function onItemSelected(chosenItem: vscode.QuickPickItem): IShowChoicePromptResponseBody { +function onItemSelected(chosenItem: vscode.QuickPickItem | undefined): IShowChoicePromptResponseBody { if (chosenItem !== undefined) { return { promptCancelled: false, @@ -155,7 +152,7 @@ function onItemSelected(chosenItem: vscode.QuickPickItem): IShowChoicePromptResp } } -function onInputEntered(responseText: string): IShowInputPromptResponseBody { +function onInputEntered(responseText: string | undefined): IShowInputPromptResponseBody { if (responseText !== undefined) { return { promptCancelled: false, @@ -171,7 +168,7 @@ function onInputEntered(responseText: string): IShowInputPromptResponseBody { export class ConsoleFeature extends LanguageClientConsumer { private commands: vscode.Disposable[]; - private handlers: vscode.Disposable[]; + private handlers: vscode.Disposable[] = []; constructor(private log: Logger) { super(); @@ -192,6 +189,10 @@ export class ConsoleFeature extends LanguageClientConsumer { } const editor = vscode.window.activeTextEditor; + if (editor === undefined) { + return; + } + let selectionRange: vscode.Range; if (!editor.selection.isEmpty) { @@ -200,7 +201,7 @@ export class ConsoleFeature extends LanguageClientConsumer { selectionRange = editor.document.lineAt(editor.selection.start.line).range; } - await this.languageClient.sendRequest(EvaluateRequestType, { + await this.languageClient?.sendRequest(EvaluateRequestType, { expression: editor.document.getText(selectionRange), }); @@ -221,12 +222,12 @@ export class ConsoleFeature extends LanguageClientConsumer { } } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; this.handlers = [ this.languageClient.onRequest( ShowChoicePromptRequestType, - (promptDetails) => showChoicePrompt(promptDetails, this.languageClient)), + (promptDetails) => showChoicePrompt(promptDetails)), this.languageClient.onRequest( ShowInputPromptRequestType, diff --git a/src/features/CustomViews.ts b/src/features/CustomViews.ts index 67d6fb7394..3f482cdd6f 100644 --- a/src/features/CustomViews.ts +++ b/src/features/CustomViews.ts @@ -27,7 +27,7 @@ export class CustomViewsFeature extends LanguageClientConsumer { } } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { languageClient.onRequest( NewCustomViewRequestType, @@ -72,7 +72,6 @@ export class CustomViewsFeature extends LanguageClientConsumer { class PowerShellContentProvider implements vscode.TextDocumentContentProvider { - private count: number = 1; private viewIndex: { [id: string]: CustomView } = {}; private didChangeEvent: vscode.EventEmitter = new vscode.EventEmitter(); @@ -100,13 +99,12 @@ class PowerShellContentProvider implements vscode.TextDocumentContentProvider { public closeView(id: string) { const uriString = this.getUri(id); - const view: CustomView = this.viewIndex[uriString]; vscode.workspace.textDocuments.some((doc) => { if (doc.uri.toString() === uriString) { vscode.window .showTextDocument(doc) - .then((editor) => vscode.commands.executeCommand("workbench.action.closeActiveEditor")); + .then((_) => vscode.commands.executeCommand("workbench.action.closeActiveEditor")); return true; } @@ -159,7 +157,7 @@ class HtmlContentView extends CustomView { styleSheetPaths: [], }; - private webviewPanel: vscode.WebviewPanel; + private webviewPanel: vscode.WebviewPanel | undefined; constructor( id: string, @@ -197,9 +195,7 @@ class HtmlContentView extends CustomView { } public showContent(viewColumn: vscode.ViewColumn): void { - if (this.webviewPanel) { - this.webviewPanel.dispose(); - } + this.webviewPanel?.dispose(); let localResourceRoots: vscode.Uri[] = []; if (this.htmlContent.javaScriptPaths) { diff --git a/src/features/DebugSession.ts b/src/features/DebugSession.ts index e10bbd78e1..b84caa1e0a 100644 --- a/src/features/DebugSession.ts +++ b/src/features/DebugSession.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import vscode = require("vscode"); -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, - ExtensionContext, WorkspaceFolder } from "vscode"; +import { + CancellationToken, DebugConfiguration, DebugConfigurationProvider, + ExtensionContext, WorkspaceFolder +} from "vscode"; import { NotificationType, RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { getPlatformDetails, OperatingSystem } from "../platform"; -import { PowerShellProcess} from "../process"; +import { PowerShellProcess } from "../process"; import { IEditorServicesSessionDetails, SessionManager, SessionStatus } from "../session"; import Settings = require("../settings"); import { Logger } from "../logging"; @@ -22,19 +22,53 @@ export const StartDebuggerNotificationType = export const StopDebuggerNotificationType = new NotificationType("powerShell/stopDebugger"); +enum DebugConfig { + LaunchCurrentFile, + LaunchScript, + InteractiveSession, + AttachHostProcess, +}; + export class DebugSessionFeature extends LanguageClientConsumer implements DebugConfigurationProvider, vscode.DebugAdapterDescriptorFactory { private sessionCount: number = 1; - private tempDebugProcess: PowerShellProcess; - private tempSessionDetails: IEditorServicesSessionDetails; - private handlers: vscode.Disposable[]; + private tempDebugProcess: PowerShellProcess | undefined; + private tempSessionDetails: IEditorServicesSessionDetails | undefined; + private handlers: vscode.Disposable[] = []; + private configs: Record = { + [DebugConfig.LaunchCurrentFile]: { + name: "PowerShell: Launch Current File", + type: "PowerShell", + request: "launch", + script: "${file}", + args: [], + }, + [DebugConfig.LaunchScript]: { + name: "PowerShell: Launch Script", + type: "PowerShell", + request: "launch", + script: "Enter path or command to execute, for example: \"${workspaceFolder}/src/foo.ps1\" or \"Invoke-Pester\"", + args: [], + }, + [DebugConfig.InteractiveSession]: { + name: "PowerShell: Interactive Session", + type: "PowerShell", + request: "launch", + }, + [DebugConfig.AttachHostProcess]: { + name: "PowerShell: Attach to PowerShell Host Process", + type: "PowerShell", + request: "attach", + runspaceId: 1, + }, + }; constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: Logger) { super(); // Register a debug configuration provider context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("PowerShell", this)); - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("PowerShell", this)) + context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("PowerShell", this)); } createDebugAdapterDescriptor( @@ -45,6 +79,11 @@ export class DebugSessionFeature extends LanguageClientConsumer ? this.tempSessionDetails : this.sessionManager.getSessionDetails(); + if (sessionDetails === undefined) { + this.logger.writeAndShowError(`No session details available for ${session.name}`); + return; + } + this.logger.writeVerbose(`Connecting to pipe: ${sessionDetails.debugServicePipeName}`); this.logger.writeVerbose(`Debug configuration: ${JSON.stringify(session.configuration)}`); @@ -57,7 +96,7 @@ export class DebugSessionFeature extends LanguageClientConsumer } } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.handlers = [ languageClient.onNotification( StartDebuggerNotificationType, @@ -78,13 +117,6 @@ export class DebugSessionFeature extends LanguageClientConsumer _folder: WorkspaceFolder | undefined, _token?: CancellationToken): Promise { - enum DebugConfig { - LaunchCurrentFile, - LaunchScript, - InteractiveSession, - AttachHostProcess, - } - const debugConfigPickItems = [ { id: DebugConfig.LaunchCurrentFile, @@ -113,51 +145,18 @@ export class DebugSessionFeature extends LanguageClientConsumer debugConfigPickItems, { placeHolder: "Select a PowerShell debug configuration" }); - // TODO: Make these available in a dictionary and share them. - switch (launchSelection.id) { - case DebugConfig.LaunchCurrentFile: - return [ - { - name: "PowerShell: Launch Current File", - type: "PowerShell", - request: "launch", - script: "${file}", - }, - ]; - case DebugConfig.LaunchScript: - return [ - { - name: "PowerShell: Launch Script", - type: "PowerShell", - request: "launch", - script: "Enter path or command to execute, for example: \"${workspaceFolder}/src/foo.ps1\" or \"Invoke-Pester\"", - }, - ]; - case DebugConfig.InteractiveSession: - return [ - { - name: "PowerShell: Interactive Session", - type: "PowerShell", - request: "launch", - }, - ]; - case DebugConfig.AttachHostProcess: - return [ - { - name: "PowerShell: Attach to PowerShell Host Process", - type: "PowerShell", - request: "attach", - runspaceId: 1, - }, - ]; + if (launchSelection) { + return [this.configs[launchSelection.id]]; } + + return [this.configs[DebugConfig.LaunchCurrentFile]]; } // DebugConfigurationProvider methods public async resolveDebugConfiguration( _folder: WorkspaceFolder | undefined, config: DebugConfiguration, - _token?: CancellationToken): Promise { + _token?: CancellationToken): Promise { // Prevent the Debug Console from opening config.internalConsoleOptions = "neverOpen"; @@ -175,19 +174,16 @@ export class DebugSessionFeature extends LanguageClientConsumer settings.debugging.createTemporaryIntegratedConsole; if (config.createTemporaryIntegratedConsole) { - this.tempDebugProcess = this.sessionManager.createDebugSessionProcess(settings); + this.tempDebugProcess = await this.sessionManager.createDebugSessionProcess(settings); this.tempSessionDetails = await this.tempDebugProcess.start(`DebugSession-${this.sessionCount++}`); } if (!config.request) { // No launch.json, create the default configuration for both unsaved // (Untitled) and saved documents. + const LaunchCurrentFileConfig = this.configs[DebugConfig.LaunchCurrentFile]; + config = { ...config, ...LaunchCurrentFileConfig }; config.current_document = true; - config.type = "PowerShell"; - config.name = "PowerShell: Launch Current File"; - config.request = "launch"; - config.args = []; - config.script = "${file}" } if (config.script === "${file}" || config.script === "${relativeFile}") { @@ -210,37 +206,40 @@ export class DebugSessionFeature extends LanguageClientConsumer public async resolveDebugConfigurationWithSubstitutedVariables( _folder: WorkspaceFolder | undefined, config: DebugConfiguration, - _token?: CancellationToken): Promise { + _token?: CancellationToken): Promise { + let resolvedConfig: DebugConfiguration | undefined | null; if (config.request === "attach") { - config = await this.resolveAttachDebugConfiguration(config); + resolvedConfig = await this.resolveAttachDebugConfiguration(config); } else if (config.request === "launch") { - config = await this.resolveLaunchDebugConfiguration(config); + resolvedConfig = await this.resolveLaunchDebugConfiguration(config); } else { vscode.window.showErrorMessage(`The request type was invalid: '${config.request}'`); return null; } - // Start the PowerShell session if needed. - if (this.sessionManager.getSessionStatus() !== SessionStatus.Running) { - await this.sessionManager.start(); + if (resolvedConfig) { + // Start the PowerShell session if needed. + if (this.sessionManager.getSessionStatus() !== SessionStatus.Running) { + await this.sessionManager.start(); + } + // Create or show the debug terminal (either temporary or session). + this.sessionManager.showDebugTerminal(true); } - // Create or show the debug terminal (either temporary or session). - this.sessionManager.showDebugTerminal(true); - - return config; + return resolvedConfig; } - private async resolveLaunchDebugConfiguration(config: DebugConfiguration): Promise { + private async resolveLaunchDebugConfiguration(config: DebugConfiguration): Promise { // Check the languageId only for current documents (which includes untitled documents). if (config.current_document) { - const currentDocument = vscode.window.activeTextEditor.document; - if (currentDocument.languageId !== "powershell") { + const currentDocument = vscode.window.activeTextEditor?.document; + if (currentDocument?.languageId !== "powershell") { vscode.window.showErrorMessage("Please change the current document's language mode to PowerShell."); return undefined; } } + // Check the temporary console setting for untitled documents only, and // check the document extension for everything else. if (config.untitled_document) { @@ -255,17 +254,24 @@ export class DebugSessionFeature extends LanguageClientConsumer return undefined; } } + return config; } - private async resolveAttachDebugConfiguration(config: DebugConfiguration): Promise { + private async resolveAttachDebugConfiguration(config: DebugConfiguration): Promise { const platformDetails = getPlatformDetails(); const versionDetails = this.sessionManager.getPowerShellVersionDetails(); + if (versionDetails === undefined) { + vscode.window.showErrorMessage(`Session version details were not found for ${config.name}`) + return null; + } + // Cross-platform attach to process was added in 6.2.0-preview.4. if (versionDetails.version < "7.0.0" && platformDetails.operatingSystem !== OperatingSystem.Windows) { vscode.window.showErrorMessage(`Attaching to a PowerShell Host Process on ${OperatingSystem[platformDetails.operatingSystem]} requires PowerShell 7.0 or higher.`); return undefined; } + // If nothing is set, prompt for the processId. if (!config.customPipeName && !config.processId) { config.processId = await vscode.commands.executeCommand("PowerShell.PickPSHostProcess"); @@ -274,6 +280,7 @@ export class DebugSessionFeature extends LanguageClientConsumer return null; } } + if (!config.runspaceId && !config.runspaceName) { config.runspaceId = await vscode.commands.executeCommand("PowerShell.PickRunspace", config.processId); // No runspace selected. Cancel attach. @@ -281,6 +288,7 @@ export class DebugSessionFeature extends LanguageClientConsumer return null; } } + return config; } } @@ -303,7 +311,7 @@ export class SpecifyScriptArgsFeature implements vscode.Disposable { this.command.dispose(); } - private async specifyScriptArguments(): Promise { + private async specifyScriptArguments(): Promise { const powerShellDbgScriptArgsKey = "powerShellDebugScriptArgs"; const options: vscode.InputBoxOptions = { @@ -338,17 +346,13 @@ interface IPSHostProcessInfo { } export const GetPSHostProcessesRequestType = - new RequestType("powerShell/getPSHostProcesses"); - -interface IGetPSHostProcessesResponseBody { - hostProcesses: IPSHostProcessInfo[]; -} + new RequestType("powerShell/getPSHostProcesses"); export class PickPSHostProcessFeature extends LanguageClientConsumer { private command: vscode.Disposable; - private waitingForClientToken: vscode.CancellationTokenSource; - private getLanguageClientResolve: (value?: LanguageClient | Promise) => void; + private waitingForClientToken?: vscode.CancellationTokenSource; + private getLanguageClientResolve?: (value: LanguageClient) => void; constructor() { super(); @@ -356,14 +360,14 @@ export class PickPSHostProcessFeature extends LanguageClientConsumer { this.command = vscode.commands.registerCommand("PowerShell.PickPSHostProcess", () => { return this.getLanguageClient() - .then((_) => this.pickPSHostProcess(), (_) => undefined); + .then((_) => this.pickPSHostProcess(), (_) => undefined); }); } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; - if (this.waitingForClientToken) { + if (this.waitingForClientToken && this.getLanguageClientResolve) { this.getLanguageClientResolve(this.languageClient); this.clearWaitingToken(); } @@ -374,7 +378,7 @@ export class PickPSHostProcessFeature extends LanguageClientConsumer { } private getLanguageClient(): Promise { - if (this.languageClient) { + if (this.languageClient !== undefined) { return Promise.resolve(this.languageClient); } else { // If PowerShell isn't finished loading yet, show a loading message @@ -389,7 +393,7 @@ export class PickPSHostProcessFeature extends LanguageClientConsumer { .showQuickPick( ["Cancel"], { placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." }, - this.waitingForClientToken.token) + this.waitingForClientToken?.token) .then((response) => { if (response === "Cancel") { this.clearWaitingToken(); @@ -412,45 +416,45 @@ export class PickPSHostProcessFeature extends LanguageClientConsumer { } } - private async pickPSHostProcess(): Promise { - const hostProcesses = await this.languageClient.sendRequest(GetPSHostProcessesRequestType, {}); + private async pickPSHostProcess(): Promise { // Start with the current PowerShell process in the list. const items: IProcessItem[] = [{ label: "Current", description: "The current PowerShell Extension process.", pid: "current", }]; - for (const p in hostProcesses) { - if (hostProcesses.hasOwnProperty(p)) { - let windowTitle = ""; - if (hostProcesses[p].mainWindowTitle) { - windowTitle = `, Title: ${hostProcesses[p].mainWindowTitle}`; - } - - items.push({ - label: hostProcesses[p].processName, - description: `PID: ${hostProcesses[p].processId.toString()}${windowTitle}`, - pid: hostProcesses[p].processId, - }); + + const response = await this.languageClient?.sendRequest(GetPSHostProcessesRequestType, {}); + for (const process of response ?? []) { + let windowTitle = ""; + if (process.mainWindowTitle) { + windowTitle = `, Title: ${process.mainWindowTitle}`; } + + items.push({ + label: process.processName, + description: `PID: ${process.processId.toString()}${windowTitle}`, + pid: process.processId, + }); } + if (items.length === 0) { return Promise.reject("There are no PowerShell host processes to attach to."); } + const options: vscode.QuickPickOptions = { placeHolder: "Select a PowerShell host process to attach to", matchOnDescription: true, matchOnDetail: true, }; const item = await vscode.window.showQuickPick(items, options); + return item ? `${item.pid}` : undefined; } private clearWaitingToken() { - if (this.waitingForClientToken) { - this.waitingForClientToken.dispose(); - this.waitingForClientToken = undefined; - } + this.waitingForClientToken?.dispose(); + this.waitingForClientToken = undefined; } } @@ -470,22 +474,22 @@ export const GetRunspaceRequestType = export class PickRunspaceFeature extends LanguageClientConsumer { private command: vscode.Disposable; - private waitingForClientToken: vscode.CancellationTokenSource; - private getLanguageClientResolve: (value?: LanguageClient | Promise) => void; + private waitingForClientToken?: vscode.CancellationTokenSource; + private getLanguageClientResolve?: (value: LanguageClient) => void; constructor() { super(); this.command = vscode.commands.registerCommand("PowerShell.PickRunspace", (processId) => { return this.getLanguageClient() - .then((_) => this.pickRunspace(processId), (_) => undefined); + .then((_) => this.pickRunspace(processId), (_) => undefined); }, this); } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; - if (this.waitingForClientToken) { + if (this.waitingForClientToken && this.getLanguageClientResolve) { this.getLanguageClientResolve(this.languageClient); this.clearWaitingToken(); } @@ -511,7 +515,7 @@ export class PickRunspaceFeature extends LanguageClientConsumer { .showQuickPick( ["Cancel"], { placeHolder: "Attach to PowerShell host process: Please wait, starting PowerShell..." }, - this.waitingForClientToken.token) + this.waitingForClientToken?.token) .then((response) => { if (response === "Cancel") { this.clearWaitingToken(); @@ -534,10 +538,10 @@ export class PickRunspaceFeature extends LanguageClientConsumer { } } - private async pickRunspace(processId: string): Promise { - const response = await this.languageClient.sendRequest(GetRunspaceRequestType, { processId }); + private async pickRunspace(processId: string): Promise { + const response = await this.languageClient?.sendRequest(GetRunspaceRequestType, { processId }); const items: IRunspaceItem[] = []; - for (const runspace of response) { + for (const runspace of response ?? []) { // Skip default runspace if ((runspace.id === 1 || runspace.name === "PSAttachRunspace") && processId === "current") { @@ -550,19 +554,19 @@ export class PickRunspaceFeature extends LanguageClientConsumer { id: runspace.id.toString(), }); } + const options: vscode.QuickPickOptions = { placeHolder: "Select PowerShell runspace to debug", matchOnDescription: true, matchOnDetail: true, }; const item = await vscode.window.showQuickPick(items, options); + return item ? `${item.id}` : undefined; } private clearWaitingToken() { - if (this.waitingForClientToken) { - this.waitingForClientToken.dispose(); - this.waitingForClientToken = undefined; - } + this.waitingForClientToken?.dispose(); + this.waitingForClientToken = undefined; } } diff --git a/src/features/ExpandAlias.ts b/src/features/ExpandAlias.ts index 5030dd5e33..62520ff3dd 100644 --- a/src/features/ExpandAlias.ts +++ b/src/features/ExpandAlias.ts @@ -4,7 +4,6 @@ import vscode = require("vscode"); import Window = vscode.window; import { RequestType } from "vscode-languageclient"; -import { Logger } from "../logging"; import { LanguageClientConsumer } from "../languageClientConsumer"; export const ExpandAliasRequestType = new RequestType("powerShell/expandAlias"); @@ -12,18 +11,21 @@ export const ExpandAliasRequestType = new RequestType("powerShel export class ExpandAliasFeature extends LanguageClientConsumer { private command: vscode.Disposable; - constructor(private log: Logger) { + constructor() { super(); this.command = vscode.commands.registerCommand("PowerShell.ExpandAlias", () => { - const editor = Window.activeTextEditor; + if (editor === undefined) { + return; + } + const document = editor.document; const selection = editor.selection; const sls = selection.start; const sle = selection.end; - let text; - let range; + let text: string | any[]; + let range: vscode.Range | vscode.Position; if ((sls.character === sle.character) && (sls.line === sle.line)) { text = document.getText(); @@ -33,7 +35,7 @@ export class ExpandAliasFeature extends LanguageClientConsumer { range = new vscode.Range(sls.line, sls.character, sle.line, sle.character); } - this.languageClient.sendRequest(ExpandAliasRequestType, { text }).then((result) => { + this.languageClient?.sendRequest(ExpandAliasRequestType, { text }).then((result) => { editor.edit((editBuilder) => { editBuilder.replace(range, result.text); }); diff --git a/src/features/ExtensionCommands.ts b/src/features/ExtensionCommands.ts index 5ca8f4c564..94242c6187 100644 --- a/src/features/ExtensionCommands.ts +++ b/src/features/ExtensionCommands.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// TODO: This file needs some TLC to use strict mode. - import * as os from "os"; import * as path from "path"; import * as vscode from "vscode"; @@ -32,8 +30,8 @@ export interface IEditorContext { currentFileContent: string; currentFileLanguage: string; currentFilePath: string; - cursorPosition: Position; - selectionRange: Range; + cursorPosition: Position | undefined | null; + selectionRange: Range | undefined | null; } export interface IInvokeExtensionCommandRequestArguments { @@ -50,16 +48,16 @@ export interface IExtensionCommandAddedNotificationBody { displayName: string; } -function asRange(value: vscode.Range): Range { +function asRange(value: vscode.Range): Range | undefined | null { if (value === undefined) { return undefined; } else if (value === null) { return null; } - return { start: asPosition(value.start), end: asPosition(value.end) }; + return { start: asPosition(value.start)!, end: asPosition(value.end)! }; } -function asPosition(value: vscode.Position): Position { +function asPosition(value: vscode.Position): Position | undefined | null { if (value === undefined) { return undefined; } else if (value === null) { @@ -68,16 +66,7 @@ function asPosition(value: vscode.Position): Position { return { line: value.line, character: value.character }; } -function asCodeRange(value: Range): vscode.Range { - if (value === undefined) { - return undefined; - } else if (value === null) { - return null; - } - return new vscode.Range(asCodePosition(value.start), asCodePosition(value.end)); -} - -function asCodePosition(value: Position): vscode.Position { +function asCodePosition(value: Position): vscode.Position | undefined | null { if (value === undefined) { return undefined; } else if (value === null) { @@ -172,19 +161,16 @@ interface IInvokeRegisteredEditorCommandParameter { export class ExtensionCommandsFeature extends LanguageClientConsumer { private commands: vscode.Disposable[]; - private handlers: vscode.Disposable[]; + private handlers: vscode.Disposable[] = []; private extensionCommands: IExtensionCommand[] = []; constructor(private log: Logger) { super(); this.commands = [ vscode.commands.registerCommand("PowerShell.ShowAdditionalCommands", async () => { - const editor = vscode.window.activeTextEditor; - let start = editor.selection.start; - if (editor.selection.isEmpty) { - start = new vscode.Position(start.line, 0); + if (this.languageClient !== undefined) { + await this.showExtensionCommands(this.languageClient); } - await this.showExtensionCommands(this.languageClient); }), vscode.commands.registerCommand("PowerShell.InvokeRegisteredEditorCommand", @@ -196,7 +182,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { const commandToExecute = this.extensionCommands.find((x) => x.name === param.commandName); if (commandToExecute) { - await this.languageClient.sendRequest( + await this.languageClient?.sendRequest( InvokeExtensionCommandRequestType, { name: commandToExecute.name, @@ -227,7 +213,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { ] } - public setLanguageClient(languageclient: LanguageClient) { + public override setLanguageClient(languageclient: LanguageClient) { // Clear the current list of extension commands since they were // only relevant to the previous session this.extensionCommands = []; @@ -320,7 +306,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { a.name.localeCompare(b.name)); } - private showExtensionCommands(client: LanguageClient): Thenable { + private async showExtensionCommands(client: LanguageClient): Promise { // If no extension commands are available, show a message if (this.extensionCommands.length === 0) { vscode.window.showInformationMessage( @@ -337,19 +323,18 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { }; }); - vscode.window - .showQuickPick( - quickPickItems, - { placeHolder: "Select a command" }) - .then((command) => this.onCommandSelected(command, client)); + const selectedCommand = await vscode.window.showQuickPick( + quickPickItems, + { placeHolder: "Select a command" }); + return this.onCommandSelected(selectedCommand, client); } private onCommandSelected( - chosenItem: IExtensionCommandQuickPickItem, - client: LanguageClient) { + chosenItem: IExtensionCommandQuickPickItem | undefined, + client: LanguageClient | undefined) { if (chosenItem !== undefined) { - client.sendRequest( + client?.sendRequest( InvokeExtensionCommandRequestType, { name: chosenItem.command.name, @@ -379,7 +364,11 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { return EditorOperationResponse.Completed; } - private getEditorContext(): IEditorContext { + private getEditorContext(): IEditorContext | undefined { + if (vscode.window.activeTextEditor === undefined) { + return undefined; + } + return { currentFileContent: vscode.window.activeTextEditor.document.getText(), currentFileLanguage: vscode.window.activeTextEditor.document.languageId, @@ -418,7 +407,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { promise = vscode.workspace.openTextDocument(filePath) .then((doc) => vscode.window.showTextDocument(doc)) - .then((editor) => vscode.commands.executeCommand("workbench.action.closeActiveEditor")) + .then((_) => vscode.commands.executeCommand("workbench.action.closeActiveEditor")) .then((_) => EditorOperationResponse.Completed); } else { promise = Promise.resolve(EditorOperationResponse.Completed); @@ -549,7 +538,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { // Make sure the file path is absolute if (!path.win32.isAbsolute(filePath)) { filePath = path.win32.resolve( - vscode.workspace.rootPath, + vscode.workspace.rootPath!, filePath); } @@ -559,7 +548,7 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { // Make sure the file path is absolute if (!path.isAbsolute(filePath)) { filePath = path.resolve( - vscode.workspace.rootPath, + vscode.workspace.rootPath!, filePath); } @@ -590,31 +579,30 @@ export class ExtensionCommandsFeature extends LanguageClientConsumer { } private setSelection(details: ISetSelectionRequestArguments): EditorOperationResponse { - vscode.window.activeTextEditor.selections = [ - new vscode.Selection( - asCodePosition(details.selectionRange.start), - asCodePosition(details.selectionRange.end)), - ]; + if (vscode.window.activeTextEditor !== undefined) { + vscode.window.activeTextEditor.selections = [ + new vscode.Selection( + asCodePosition(details.selectionRange.start)!, + asCodePosition(details.selectionRange.end)!), + ]; + } return EditorOperationResponse.Completed; } - private showInformationMessage(message: string): Thenable { - return vscode.window - .showInformationMessage(message) - .then((_) => EditorOperationResponse.Completed); + private async showInformationMessage(message: string): Promise { + await vscode.window.showInformationMessage(message); + return EditorOperationResponse.Completed; } - private showErrorMessage(message: string): Thenable { - return vscode.window - .showErrorMessage(message) - .then((_) => EditorOperationResponse.Completed); + private async showErrorMessage(message: string): Promise { + await vscode.window.showErrorMessage(message); + return EditorOperationResponse.Completed; } - private showWarningMessage(message: string): Thenable { - return vscode.window - .showWarningMessage(message) - .then((_) => EditorOperationResponse.Completed); + private async showWarningMessage(message: string): Promise { + await vscode.window.showWarningMessage(message); + return EditorOperationResponse.Completed; } private setStatusBarMessage(messageDetails: IStatusBarMessageDetails): EditorOperationResponse { diff --git a/src/features/ExternalApi.ts b/src/features/ExternalApi.ts index 252da1ed16..c70d5ae040 100644 --- a/src/features/ExternalApi.ts +++ b/src/features/ExternalApi.ts @@ -112,7 +112,7 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower } // TODO: When we have more than one API version, make sure to include a check here. - return ExternalApiFeature.registeredExternalExtension.get(uuid); + return ExternalApiFeature.registeredExternalExtension.get(uuid)!; } /* @@ -141,10 +141,10 @@ export class ExternalApiFeature extends LanguageClientConsumer implements IPower const versionDetails = this.sessionManager.getPowerShellVersionDetails(); return { - exePath: this.sessionManager.PowerShellExeDetails.exePath, - version: versionDetails.version, - displayName: this.sessionManager.PowerShellExeDetails.displayName, // comes from the Session Menu - architecture: versionDetails.architecture + exePath: this.sessionManager.PowerShellExeDetails?.exePath ?? "unknown", + version: versionDetails?.version ?? "unknown", + displayName: this.sessionManager.PowerShellExeDetails?.displayName ?? "unknown", // comes from the Session Menu + architecture: versionDetails?.architecture ?? "unknown" }; } /* diff --git a/src/features/FindModule.ts b/src/features/FindModule.ts index 1a50580ffd..46ade452c6 100644 --- a/src/features/FindModule.ts +++ b/src/features/FindModule.ts @@ -15,7 +15,7 @@ export const InstallModuleRequestType = export class FindModuleFeature extends LanguageClientConsumer { private command: vscode.Disposable; - private cancelFindToken: vscode.CancellationTokenSource; + private cancelFindToken?: vscode.CancellationTokenSource; constructor() { super(); @@ -24,9 +24,9 @@ export class FindModuleFeature extends LanguageClientConsumer { this.cancelFindToken = new vscode.CancellationTokenSource(); vscode.window .showQuickPick( - ["Cancel"], - { placeHolder: "Please wait, retrieving list of PowerShell modules. This can take some time..." }, - this.cancelFindToken.token) + ["Cancel"], + { placeHolder: "Please wait, retrieving list of PowerShell modules. This can take some time..." }, + this.cancelFindToken.token) .then((response) => { if (response === "Cancel") { this.clearCancelFindToken(); } }); @@ -44,8 +44,7 @@ export class FindModuleFeature extends LanguageClientConsumer { this.pickPowerShellModule().then((moduleName) => { if (moduleName) { - // vscode.window.setStatusBarMessage("Installing PowerShell Module " + moduleName, 1500); - this.languageClient.sendRequest(InstallModuleRequestType, moduleName); + this.languageClient?.sendRequest(InstallModuleRequestType, moduleName); } }); }); @@ -55,44 +54,43 @@ export class FindModuleFeature extends LanguageClientConsumer { this.command.dispose(); } - private pickPowerShellModule(): Thenable { - return this.languageClient.sendRequest(FindModuleRequestType, null).then((modules) => { - const items: QuickPickItem[] = []; + private async pickPowerShellModule(): Promise { + const modules = await this.languageClient?.sendRequest(FindModuleRequestType, undefined); + const items: QuickPickItem[] = []; - // We've got the modules info, let's cancel the timeout unless it's already been cancelled - if (this.cancelFindToken) { - this.clearCancelFindToken(); - } else { - // Already timed out, would be weird to dislay modules after we said it timed out. - return Promise.resolve(""); - } + // We've got the modules info, let's cancel the timeout unless it's already been cancelled + if (this.cancelFindToken) { + this.clearCancelFindToken(); + } else { + // Already timed out, would be weird to display modules after we said it timed out. + return Promise.resolve(""); + } + if (modules !== undefined) { for (const item in modules) { if (modules.hasOwnProperty(item)) { items.push({ label: modules[item].name, description: modules[item].description }); } } + } - if (items.length === 0) { - return Promise.reject("No PowerShell modules were found."); - } + if (items.length === 0) { + return Promise.reject("No PowerShell modules were found."); + } - const options: vscode.QuickPickOptions = { - placeHolder: "Select a PowerShell module to install", - matchOnDescription: true, - matchOnDetail: true, - }; + const options: vscode.QuickPickOptions = { + placeHolder: "Select a PowerShell module to install", + matchOnDescription: true, + matchOnDetail: true, + }; - return vscode.window.showQuickPick(items, options).then((item) => { - return item ? item.label : ""; - }); + return vscode.window.showQuickPick(items, options).then((item) => { + return item ? item.label : ""; }); } private clearCancelFindToken() { - if (this.cancelFindToken) { - this.cancelFindToken.dispose(); - this.cancelFindToken = undefined; - } + this.cancelFindToken?.dispose(); + this.cancelFindToken = undefined; } } diff --git a/src/features/GenerateBugReport.ts b/src/features/GenerateBugReport.ts index ec1b8b1da2..e819324c34 100644 --- a/src/features/GenerateBugReport.ts +++ b/src/features/GenerateBugReport.ts @@ -14,11 +14,11 @@ const issuesUrl: string = `${project}/issues/new`; const extensions = vscode.extensions.all.filter((element) => element.packageJSON.isBuiltin === false) - .sort((leftside, rightside): number => { - if (leftside.packageJSON.name.toLowerCase() < rightside.packageJSON.name.toLowerCase()) { + .sort((leftSide, rightSide): number => { + if (leftSide.packageJSON.name.toLowerCase() < rightSide.packageJSON.name.toLowerCase()) { return -1; } - if (leftside.packageJSON.name.toLowerCase() > rightside.packageJSON.name.toLowerCase()) { + if (leftSide.packageJSON.name.toLowerCase() > rightSide.packageJSON.name.toLowerCase()) { return 1; } return 0; @@ -79,7 +79,7 @@ ${this.generateExtensionTable(extensions)} this.command.dispose(); } - private generateExtensionTable(installedExtensions): string { + private generateExtensionTable(installedExtensions: vscode.Extension[]): string { if (!installedExtensions.length) { return "none"; } @@ -89,6 +89,7 @@ ${this.generateExtensionTable(extensions)} if (e.packageJSON.isBuiltin === false) { return `|${e.packageJSON.name}|${e.packageJSON.publisher}|${e.packageJSON.version}|`; } + return undefined; }).join("\n"); const extensionTable = ` @@ -104,8 +105,7 @@ ${tableHeader}\n${table}; } private getRuntimeInfo() { - - const powerShellExePath = this.sessionManager.PowerShellExeDetails.exePath; + const powerShellExePath = this.sessionManager.PowerShellExeDetails?.exePath; const powerShellArgs = [ "-NoProfile", "-Command", diff --git a/src/features/GetCommands.ts b/src/features/GetCommands.ts index c97ce92a1b..b34a30b00d 100644 --- a/src/features/GetCommands.ts +++ b/src/features/GetCommands.ts @@ -39,7 +39,7 @@ export class GetCommandsFeature extends LanguageClientConsumer { { treeDataProvider: this.commandsExplorerProvider }); // Refresh the command explorer when the view is visible - this.commandsExplorerTreeView.onDidChangeVisibility( (e) => { + this.commandsExplorerTreeView.onDidChangeVisibility((e) => { if (e.visible) { this.CommandExplorerRefresh(); } @@ -52,7 +52,7 @@ export class GetCommandsFeature extends LanguageClientConsumer { this.command.dispose(); } - public setLanguageClient(languageclient: LanguageClient) { + public override setLanguageClient(languageclient: LanguageClient) { this.languageClient = languageclient; if (this.commandsExplorerTreeView.visible) { vscode.commands.executeCommand("PowerShell.RefreshCommandsExplorer"); @@ -66,15 +66,19 @@ export class GetCommandsFeature extends LanguageClientConsumer { } this.languageClient.sendRequest(GetCommandRequestType).then((result) => { const SidebarConfig = vscode.workspace.getConfiguration("powershell.sideBar"); - const excludeFilter = (SidebarConfig.CommandExplorerExcludeFilter).map((filter) => filter.toLowerCase()); + const excludeFilter = (SidebarConfig.CommandExplorerExcludeFilter).map((filter: string) => filter.toLowerCase()); result = result.filter((command) => (excludeFilter.indexOf(command.moduleName.toLowerCase()) === -1)); this.commandsExplorerProvider.powerShellCommands = result.map(toCommand); this.commandsExplorerProvider.refresh(); }); } - private InsertCommand(item) { + private InsertCommand(item: { Name: string; }) { const editor = vscode.window.activeTextEditor; + if (editor === undefined) { + return; + } + const sls = editor.selection.start; const sle = editor.selection.end; const range = new vscode.Range(sls.line, sls.character, sle.line, sle.character); @@ -86,7 +90,7 @@ export class GetCommandsFeature extends LanguageClientConsumer { class CommandsExplorerProvider implements vscode.TreeDataProvider { public readonly onDidChangeTreeData: vscode.Event; - public powerShellCommands: Command[]; + public powerShellCommands: Command[] = []; private didChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); constructor() { @@ -101,7 +105,7 @@ class CommandsExplorerProvider implements vscode.TreeDataProvider { return element; } - public getChildren(element?: Command): Thenable { + public getChildren(_element?: Command): Thenable { return Promise.resolve(this.powerShellCommands || []); } } @@ -123,7 +127,7 @@ class Command extends vscode.TreeItem { public readonly defaultParameterSet: string, public readonly ParameterSets: object, public readonly Parameters: object, - public readonly collapsibleState = vscode.TreeItemCollapsibleState.None, + public override readonly collapsibleState = vscode.TreeItemCollapsibleState.None, ) { super(Name, collapsibleState); } @@ -135,7 +139,7 @@ class Command extends vscode.TreeItem { }; } - public async getChildren(element?): Promise { + public async getChildren(_element?: any): Promise { return []; // Returning an empty array because we need to return something. } diff --git a/src/features/HelpCompletion.ts b/src/features/HelpCompletion.ts index 14263d10b1..b0c544dbe1 100644 --- a/src/features/HelpCompletion.ts +++ b/src/features/HelpCompletion.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Disposable, EndOfLine, Position, Range, SnippetString, - TextDocument, TextDocumentChangeEvent, window, workspace } from "vscode"; +import { + Disposable, EndOfLine, Range, SnippetString, + TextDocument, TextDocumentChangeEvent, window, workspace +} from "vscode"; import { RequestType } from "vscode-languageclient"; import { LanguageClient } from "vscode-languageclient/node"; import { Logger } from "../logging"; @@ -12,21 +14,11 @@ import { LanguageClientConsumer } from "../languageClientConsumer"; export const CommentHelpRequestType = new RequestType("powerShell/getCommentHelp"); -interface ICommentHelpRequestParams { - documentUri: string; - triggerPosition: Position; - blockComment: boolean; -} - -interface ICommentHelpRequestResult { - content: string[]; -} - enum SearchState { Searching, Locked, Found } export class HelpCompletionFeature extends LanguageClientConsumer { - private helpCompletionProvider: HelpCompletionProvider; - private disposable: Disposable; + private helpCompletionProvider: HelpCompletionProvider | undefined; + private disposable: Disposable | undefined; private settings: Settings.ISettings; constructor(private log: Logger) { @@ -35,19 +27,17 @@ export class HelpCompletionFeature extends LanguageClientConsumer { if (this.settings.helpCompletion !== Settings.CommentType.Disabled) { this.helpCompletionProvider = new HelpCompletionProvider(); - const subscriptions = []; + const subscriptions: Disposable[] = []; workspace.onDidChangeTextDocument(this.onEvent, this, subscriptions); this.disposable = Disposable.from(...subscriptions); } } public dispose() { - if (this.disposable) { - this.disposable.dispose(); - } + this.disposable?.dispose(); } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; if (this.helpCompletionProvider) { this.helpCompletionProvider.languageClient = languageClient; @@ -67,15 +57,15 @@ export class HelpCompletionFeature extends LanguageClientConsumer { } if (changeEvent.contentChanges.length > 0) { - this.helpCompletionProvider.updateState( + this.helpCompletionProvider?.updateState( changeEvent.document, changeEvent.contentChanges[0].text, changeEvent.contentChanges[0].range); // todo raise an event when trigger is found, and attach complete() to the event. - if (this.helpCompletionProvider.triggerFound) { + if (this.helpCompletionProvider?.triggerFound) { await this.helpCompletionProvider.complete(); - await this.helpCompletionProvider.reset(); + this.helpCompletionProvider.reset(); } } } @@ -83,7 +73,7 @@ export class HelpCompletionFeature extends LanguageClientConsumer { class TriggerFinder { private state: SearchState; - private document: TextDocument; + private document: TextDocument | undefined; private count: number; constructor(private triggerCharacters: string) { @@ -107,8 +97,8 @@ class TriggerFinder { case SearchState.Locked: if (document === this.document && - changeText.length === 1 && - changeText[0] === this.triggerCharacters[this.count]) { + changeText.length === 1 && + changeText[0] === this.triggerCharacters[this.count]) { this.count++; if (this.count === this.triggerCharacters.length) { this.state = SearchState.Found; @@ -132,9 +122,9 @@ class TriggerFinder { class HelpCompletionProvider { private triggerFinderHelpComment: TriggerFinder; - private lastChangeRange: Range; - private lastDocument: TextDocument; - private langClient: LanguageClient; + private lastChangeRange: Range | undefined; + private lastDocument: TextDocument | undefined; + private langClient: LanguageClient | undefined; private settings: Settings.ISettings; constructor() { @@ -161,7 +151,7 @@ class HelpCompletionProvider { } public async complete(): Promise { - if (this.langClient === undefined) { + if (this.langClient === undefined || this.lastChangeRange === undefined || this.lastDocument === undefined) { return; } @@ -190,7 +180,7 @@ class HelpCompletionProvider { const snippetString = new SnippetString(text); - window.activeTextEditor.insertSnippet(snippetString, replaceRange); + window.activeTextEditor?.insertSnippet(snippetString, replaceRange); } private getEOL(eol: EndOfLine): string { diff --git a/src/features/NewFileOrProject.ts b/src/features/NewFileOrProject.ts index 77ebe0ae6d..1f8245160a 100644 --- a/src/features/NewFileOrProject.ts +++ b/src/features/NewFileOrProject.ts @@ -10,7 +10,7 @@ export class NewFileOrProjectFeature extends LanguageClientConsumer { private readonly loadIcon = " $(sync) "; private command: vscode.Disposable; - private waitingForClientToken: vscode.CancellationTokenSource; + private waitingForClientToken?: vscode.CancellationTokenSource; constructor() { super(); @@ -35,13 +35,13 @@ export class NewFileOrProjectFeature extends LanguageClientConsumer { // Cancel the loading prompt after 60 seconds setTimeout(() => { - if (this.waitingForClientToken) { - this.clearWaitingToken(); + if (this.waitingForClientToken) { + this.clearWaitingToken(); - vscode.window.showErrorMessage( - "New Project: PowerShell session took too long to start."); - } - }, 60000); + vscode.window.showErrorMessage( + "New Project: PowerShell session took too long to start."); + } + }, 60000); } else { this.showProjectTemplates(); } @@ -52,7 +52,7 @@ export class NewFileOrProjectFeature extends LanguageClientConsumer { this.command.dispose(); } - public setLanguageClient(languageClient: LanguageClient) { + public override setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; if (this.waitingForClientToken) { @@ -61,98 +61,96 @@ export class NewFileOrProjectFeature extends LanguageClientConsumer { } } - private showProjectTemplates(includeInstalledModules: boolean = false): void { - vscode.window - .showQuickPick( - this.getProjectTemplates(includeInstalledModules), - { placeHolder: "Choose a template to create a new project", - ignoreFocusOut: true }) - .then((template) => { - if (template.label.startsWith(this.loadIcon)) { - this.showProjectTemplates(true); - } else { - this.createProjectFromTemplate(template.template); - } + private async showProjectTemplates(includeInstalledModules: boolean = false): Promise { + const template = await vscode.window.showQuickPick( + this.getProjectTemplates(includeInstalledModules), + { + placeHolder: "Choose a template to create a new project", + ignoreFocusOut: true }); + + if (template === undefined) { + return; + } else if (template.label.startsWith(this.loadIcon)) { + this.showProjectTemplates(true); + } else if (template.template) { + this.createProjectFromTemplate(template.template); + } } - private getProjectTemplates(includeInstalledModules: boolean): Thenable { - return this.languageClient - .sendRequest(GetProjectTemplatesRequestType, { includeInstalledModules }) - .then((response) => { - if (response.needsModuleInstall) { - // TODO: Offer to install Plaster - vscode.window.showErrorMessage("Plaster is not installed!"); - return Promise.reject("Plaster needs to be installed"); - } else { - let templates = response.templates.map( - (template) => { - return { - label: template.title, - description: `v${template.version} by ${template.author}, tags: ${template.tags}`, - detail: template.description, - template, - }; - }); + private async getProjectTemplates(includeInstalledModules: boolean): Promise { + if (this.languageClient === undefined) { + return Promise.reject("Language client not defined!") + } - if (!includeInstalledModules) { - templates = - [({ - label: this.loadIcon, - description: "Load additional templates from installed modules", - template: undefined, - } as ITemplateQuickPickItem)] - .concat(templates); - } else { - templates = - [({ - label: this.loadIcon, - description: "Refresh template list", - template: undefined, - } as ITemplateQuickPickItem)] - .concat(templates); - } - - return templates; - } - }); + const response = await this.languageClient.sendRequest( + GetProjectTemplatesRequestType, + { includeInstalledModules }); + + if (response.needsModuleInstall) { + // TODO: Offer to install Plaster + vscode.window.showErrorMessage("Plaster is not installed!"); + return Promise.reject("Plaster needs to be installed"); + } else { + let templates = response.templates.map( + (template) => { + return { + label: template.title, + description: `v${template.version} by ${template.author}, tags: ${template.tags}`, + detail: template.description, + template, + }; + }); + + if (!includeInstalledModules) { + templates = + [({ + label: this.loadIcon, + description: "Load additional templates from installed modules", + template: undefined, + } as ITemplateQuickPickItem)] + .concat(templates); + } else { + templates = + [({ + label: this.loadIcon, + description: "Refresh template list", + template: undefined, + } as ITemplateQuickPickItem)] + .concat(templates); + } + + return templates; + } } - private createProjectFromTemplate(template: ITemplateDetails): void { - vscode.window - .showInputBox( - { placeHolder: "Enter an absolute path to the folder where the project should be created", - ignoreFocusOut: true }) - .then((destinationPath) => { - - if (destinationPath) { - // Show the PowerShell session output in case an error occurred - vscode.commands.executeCommand("PowerShell.ShowSessionOutput"); - - this.languageClient - .sendRequest( - NewProjectFromTemplateRequestType, - { templatePath: template.templatePath, destinationPath }) - .then((result) => { - if (result.creationSuccessful) { - this.openWorkspacePath(destinationPath); - } else { - vscode.window.showErrorMessage( - "Project creation failed, read the Output window for more details."); - } - }); - } else { - vscode.window - .showErrorMessage( - "New Project: You must enter an absolute folder path to continue. Try again?", - "Yes", "No") - .then((response) => { - if (response === "Yes") { - this.createProjectFromTemplate(template); - } - }); - } + private async createProjectFromTemplate(template: ITemplateDetails): Promise { + const destinationPath = await vscode.window.showInputBox( + { + placeHolder: "Enter an absolute path to the folder where the project should be created", + ignoreFocusOut: true }); + + if (destinationPath !== undefined) { + // Show the PowerShell session output in case an error occurred + vscode.commands.executeCommand("PowerShell.ShowSessionOutput"); + + const result = await this.languageClient?.sendRequest( + NewProjectFromTemplateRequestType, + { templatePath: template.templatePath, destinationPath }); + if (result?.creationSuccessful) { + this.openWorkspacePath(destinationPath); + } else { + await vscode.window.showErrorMessage("Project creation failed, read the Output window for more details."); + } + } else { + const response = await vscode.window.showErrorMessage( + "New Project: You must enter an absolute folder path to continue. Try again?", + "Yes", "No"); + if (response === "Yes") { + this.createProjectFromTemplate(template); + } + } } private openWorkspacePath(workspacePath: string) { @@ -164,15 +162,13 @@ export class NewFileOrProjectFeature extends LanguageClientConsumer { } private clearWaitingToken() { - if (this.waitingForClientToken) { - this.waitingForClientToken.dispose(); - this.waitingForClientToken = undefined; - } + this.waitingForClientToken?.dispose(); + this.waitingForClientToken = undefined; } } interface ITemplateQuickPickItem extends vscode.QuickPickItem { - template: ITemplateDetails; + template?: ITemplateDetails; } interface ITemplateDetails { @@ -201,11 +197,6 @@ export const NewProjectFromTemplateRequestType = new RequestType( "powerShell/newProjectFromTemplate"); -interface INewProjectFromTemplateRequestArgs { - destinationPath: string; - templatePath: string; -} - interface INewProjectFromTemplateResponseBody { creationSuccessful: boolean; } diff --git a/src/features/OpenInISE.ts b/src/features/OpenInISE.ts index 13199bbe30..3c12b5368f 100644 --- a/src/features/OpenInISE.ts +++ b/src/features/OpenInISE.ts @@ -9,11 +9,13 @@ export class OpenInISEFeature implements vscode.Disposable { constructor() { this.command = vscode.commands.registerCommand("PowerShell.OpenInISE", () => { - const editor = vscode.window.activeTextEditor; + if (editor === undefined) { + return; + } + const document = editor.document; const uri = document.uri; - let ISEPath = process.env.windir; if (process.env.hasOwnProperty("PROCESSOR_ARCHITEW6432")) { diff --git a/src/features/PesterTests.ts b/src/features/PesterTests.ts index 7cda274f1a..8c23a68a42 100644 --- a/src/features/PesterTests.ts +++ b/src/features/PesterTests.ts @@ -48,7 +48,7 @@ export class PesterTestsFeature implements vscode.Disposable { launchType: LaunchType, fileUri: vscode.Uri): Promise { - const uriString = (fileUri || vscode.window.activeTextEditor.document.uri).toString(); + const uriString = (fileUri || vscode.window.activeTextEditor?.document.uri).toString(); const launchConfig = this.createLaunchConfig(uriString, launchType); return this.launch(launchConfig); } @@ -133,7 +133,7 @@ export class PesterTestsFeature implements vscode.Disposable { // // Ensure the necessary script exists (for testing). The debugger will // start regardless, but we also pass its success along. - return utils.checkIfFileExists(this.invokePesterStubScriptPath) + return await utils.checkIfFileExists(this.invokePesterStubScriptPath) && vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], launchConfig); } } diff --git a/src/features/RemoteFiles.ts b/src/features/RemoteFiles.ts index ef2c5af3c9..82a398edf9 100644 --- a/src/features/RemoteFiles.ts +++ b/src/features/RemoteFiles.ts @@ -60,15 +60,15 @@ export class RemoteFilesFeature extends LanguageClientConsumer { const remoteDocuments = vscode.workspace.textDocuments.filter((doc) => this.isDocumentRemote(doc)); - function innerCloseFiles(): Thenable<{}> { - if (remoteDocuments.length > 0) { - const doc = remoteDocuments.pop(); - - return vscode.window - .showTextDocument(doc) - .then((editor) => vscode.commands.executeCommand("workbench.action.closeActiveEditor")) - .then((_) => innerCloseFiles()); + async function innerCloseFiles(): Promise { + const doc = remoteDocuments.pop(); + if (doc === undefined) { + return; } + + await vscode.window.showTextDocument(doc); + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + return await innerCloseFiles(); } innerCloseFiles(); diff --git a/src/features/ShowHelp.ts b/src/features/ShowHelp.ts index ebb6662732..d0a0450ba9 100644 --- a/src/features/ShowHelp.ts +++ b/src/features/ShowHelp.ts @@ -3,7 +3,6 @@ import vscode = require("vscode"); import { NotificationType } from "vscode-languageclient"; -import { Logger } from "../logging"; import { LanguageClientConsumer } from "../languageClientConsumer"; export const ShowHelpNotificationType = @@ -12,21 +11,24 @@ export const ShowHelpNotificationType = export class ShowHelpFeature extends LanguageClientConsumer { private command: vscode.Disposable; - constructor(private log: Logger) { + constructor() { super(); this.command = vscode.commands.registerCommand("PowerShell.ShowHelp", (item?) => { if (!item || !item.Name) { const editor = vscode.window.activeTextEditor; + if (editor === undefined) { + return; + } const selection = editor.selection; const doc = editor.document; const cwr = doc.getWordRangeAtPosition(selection.active); const text = doc.getText(cwr); - this.languageClient.sendNotification(ShowHelpNotificationType, { text }); + this.languageClient?.sendNotification(ShowHelpNotificationType, { text }); } else { - this.languageClient.sendNotification(ShowHelpNotificationType, { text: item.Name } ); + this.languageClient?.sendNotification(ShowHelpNotificationType, { text: item.Name }); } }); } diff --git a/src/features/UpdatePowerShell.ts b/src/features/UpdatePowerShell.ts index c9bb7d4ff6..f183a6d099 100644 --- a/src/features/UpdatePowerShell.ts +++ b/src/features/UpdatePowerShell.ts @@ -20,7 +20,7 @@ import { EvaluateRequestType } from "./Console"; const streamPipeline = util.promisify(stream.pipeline); const PowerShellGitHubReleasesUrl = - "https://api.github.com/repos/PowerShell/PowerShell/releases/latest"; + "https://api.github.com/repos/PowerShell/PowerShell/releases/latest"; const PowerShellGitHubPrereleasesUrl = "https://api.github.com/repos/PowerShell/PowerShell/releases"; @@ -62,7 +62,7 @@ export class GitHubReleaseInformation { public assets: any[]; public constructor(version: string | semver.SemVer, assets: any[] = []) { - this.version = semver.parse(version); + this.version = semver.parse(version)!; if (semver.prerelease(this.version)) { this.isPreview = true; @@ -102,11 +102,9 @@ export async function InvokePowerShellUpdateCheck( return; } - const commonText: string = `You have an old version of PowerShell (${ - localVersion.raw - }). The current latest release is ${ - release.version.raw - }.`; + const commonText: string = `You have an old version of PowerShell (${localVersion.raw + }). The current latest release is ${release.version.raw + }.`; if (process.platform === "linux") { await window.showInformationMessage( @@ -115,9 +113,8 @@ export async function InvokePowerShellUpdateCheck( } const result = await window.showInformationMessage( - `${commonText} Would you like to update the version? ${ - isMacOS ? "(Homebrew is required on macOS)" - : "(This will close ALL pwsh terminals running in this Visual Studio Code session)" + `${commonText} Would you like to update the version? ${isMacOS ? "(Homebrew is required on macOS)" + : "(This will close ALL pwsh terminals running in this Visual Studio Code session)" }`, ...options); // If the user cancels the notification. @@ -144,10 +141,10 @@ export async function InvokePowerShellUpdateCheck( location: ProgressLocation.Notification, cancellable: false, }, - async () => { - // Streams the body of the request to a file. - await streamPipeline(res.body, fs.createWriteStream(msiDownloadPath)); - }); + async () => { + // Streams the body of the request to a file. + await streamPipeline(res.body, fs.createWriteStream(msiDownloadPath)); + }); // Stop the session because Windows likes to hold on to files. sessionManager.stop(); diff --git a/src/languageClientConsumer.ts b/src/languageClientConsumer.ts index b1cea31ac0..d191c366c1 100644 --- a/src/languageClientConsumer.ts +++ b/src/languageClientConsumer.ts @@ -6,7 +6,7 @@ import { LanguageClient } from "vscode-languageclient/node"; export abstract class LanguageClientConsumer { - private _languageClient: LanguageClient; + private _languageClient: LanguageClient | undefined; public setLanguageClient(languageClient: LanguageClient) { this.languageClient = languageClient; @@ -14,7 +14,7 @@ export abstract class LanguageClientConsumer { abstract dispose(): void; - public get languageClient(): LanguageClient { + public get languageClient(): LanguageClient | undefined { if (!this._languageClient) { window.showInformationMessage( "PowerShell extension has not finished starting up yet. Please try again in a few moments."); @@ -22,7 +22,7 @@ export abstract class LanguageClientConsumer { return this._languageClient; } - public set languageClient(value: LanguageClient) { + public set languageClient(value: LanguageClient | undefined) { this._languageClient = value; } } diff --git a/src/logging.ts b/src/logging.ts index b61beaf9e3..57955f4b76 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import utils = require("./utils"); import os = require("os"); import vscode = require("vscode"); @@ -30,12 +28,12 @@ export interface ILogger { export class Logger implements ILogger { public logBasePath: vscode.Uri; - public logSessionPath: vscode.Uri; + public logSessionPath: vscode.Uri | undefined; public MinimumLogLevel: LogLevel = LogLevel.Normal; private commands: vscode.Disposable[]; private logChannel: vscode.OutputChannel; - private logFilePath: vscode.Uri; + private logFilePath: vscode.Uri | undefined; constructor(logBasePath: vscode.Uri) { this.logChannel = vscode.window.createOutputChannel("PowerShell Extension Logs"); @@ -59,7 +57,7 @@ export class Logger implements ILogger { } public getLogFilePath(baseName: string): vscode.Uri { - return vscode.Uri.joinPath(this.logSessionPath, `${baseName}.log`); + return vscode.Uri.joinPath(this.logSessionPath!, `${baseName}.log`); } private writeAtLevel(logLevel: LogLevel, message: string, ...additionalMessages: string[]): void { @@ -135,7 +133,7 @@ export class Logger implements ILogger { } public async startNewLog(minimumLogLevel: string = "Normal"): Promise { - this.MinimumLogLevel = this.logLevelNameToValue(minimumLogLevel.trim()); + this.MinimumLogLevel = Logger.logLevelNameToValue(minimumLogLevel); this.logSessionPath = vscode.Uri.joinPath( @@ -146,8 +144,9 @@ export class Logger implements ILogger { await vscode.workspace.fs.createDirectory(this.logSessionPath); } - private logLevelNameToValue(logLevelName: string): LogLevel { - switch (logLevelName.toLowerCase()) { + // TODO: Make the enum smarter about strings so this goes away. + public static logLevelNameToValue(logLevelName: string): LogLevel { + switch (logLevelName.trim().toLowerCase()) { case "diagnostic": return LogLevel.Diagnostic; case "verbose": return LogLevel.Verbose; case "normal": return LogLevel.Normal; diff --git a/src/main.ts b/src/main.ts index c35e92cc60..531717fec4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import vscode = require("vscode"); import TelemetryReporter from "@vscode/extension-telemetry"; import { DocumentSelector } from "vscode-languageclient"; @@ -27,7 +25,7 @@ import { RemoteFilesFeature } from "./features/RemoteFiles"; import { RunCodeFeature } from "./features/RunCode"; import { ShowHelpFeature } from "./features/ShowHelp"; import { SpecifyScriptArgsFeature } from "./features/DebugSession"; -import { Logger, LogLevel } from "./logging"; +import { Logger } from "./logging"; import { SessionManager } from "./session"; import Settings = require("./settings"); import { PowerShellLanguageId } from "./utils"; @@ -65,7 +63,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { + public async getFirstAvailablePowerShellInstallation(): Promise { for await (const pwsh of this.enumeratePowerShellInstallations()) { return pwsh; } + return undefined; } /** @@ -121,7 +122,7 @@ export class PowerShellExeFinder { * Fixes PowerShell paths when Windows PowerShell is set to the non-native bitness. * @param configuredPowerShellPath the PowerShell path configured by the user. */ - public fixWindowsPowerShellPath(configuredPowerShellPath: string): string { + public fixWindowsPowerShellPath(configuredPowerShellPath: string): string | undefined { const altWinPS = this.findWinPS({ useAlternateBitness: true }); if (!altWinPS) { @@ -132,7 +133,7 @@ export class PowerShellExeFinder { const lowerConfiguredPath = configuredPowerShellPath.toLocaleLowerCase(); if (lowerConfiguredPath === lowerAltWinPSPath) { - return this.findWinPS().exePath; + return this.findWinPS()?.exePath; } return configuredPowerShellPath; @@ -166,7 +167,7 @@ export class PowerShellExeFinder { * Returned values may not exist, but come with an .exists property * which will check whether the executable exists. */ - private async *enumerateDefaultPowerShellInstallations(): AsyncIterable { + private async *enumerateDefaultPowerShellInstallations(): AsyncIterable { // Find PSCore stable first yield this.findPSCoreStable(); @@ -234,7 +235,7 @@ export class PowerShellExeFinder { } } - private async findPSCoreStable(): Promise { + private async findPSCoreStable(): Promise { switch (this.platformDetails.operatingSystem) { case OperatingSystem.Linux: return new PossiblePowerShellExe(LinuxExePath, "PowerShell"); @@ -244,10 +245,13 @@ export class PowerShellExeFinder { case OperatingSystem.Windows: return await this.findPSCoreWindowsInstallation(); + + case OperatingSystem.Unknown: + return undefined; } } - private async findPSCorePreview(): Promise { + private async findPSCorePreview(): Promise { switch (this.platformDetails.operatingSystem) { case OperatingSystem.Linux: return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview"); @@ -257,6 +261,9 @@ export class PowerShellExeFinder { case OperatingSystem.Windows: return await this.findPSCoreWindowsInstallation({ findPreview: true }); + + case OperatingSystem.Unknown: + return undefined; } } @@ -271,17 +278,17 @@ export class PowerShellExeFinder { return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool", undefined, false); } - private async findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise { + private async findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise { // We can't proceed if there's no LOCALAPPDATA path if (!process.env.LOCALAPPDATA) { - return null; + return undefined; } // Find the base directory for MSIX application exe shortcuts const msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps"); if (!await utils.checkIfDirectoryExists(msixAppDir)) { - return null; + return undefined; } // Define whether we're looking for the preview or the stable @@ -297,8 +304,7 @@ export class PowerShellExeFinder { } } - // If we find nothing, return null - return null; + return undefined; } private findPSCoreStableSnap(): IPossiblePowerShellExe { @@ -311,23 +317,23 @@ export class PowerShellExeFinder { private async findPSCoreWindowsInstallation( { useAlternateBitness = false, findPreview = false }: - { useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise { + { useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise { - const programFilesPath: string = this.getProgramFilesPath({ useAlternateBitness }); + const programFilesPath = this.getProgramFilesPath({ useAlternateBitness }); if (!programFilesPath) { - return null; + return undefined; } const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell"); // Ensure the base directory exists if (!await utils.checkIfDirectoryExists(powerShellInstallBaseDir)) { - return null; + return undefined; } let highestSeenVersion: number = -1; - let pwshExePath: string = null; + let pwshExePath: string | undefined; for (const item of await utils.readDirectory(powerShellInstallBaseDir)) { let currentVersion: number = -1; if (findPreview) { @@ -376,7 +382,7 @@ export class PowerShellExeFinder { } if (!pwshExePath) { - return null; + return undefined; } const bitness: string = programFilesPath.includes("x86") @@ -388,16 +394,20 @@ export class PowerShellExeFinder { return new PossiblePowerShellExe(pwshExePath, `PowerShell${preview} ${bitness}`); } - private findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe { + private findWinPS({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): IPossiblePowerShellExe | undefined { // 32-bit OSes only have one WinPS on them if (!this.platformDetails.isOS64Bit && useAlternateBitness) { - return null; + return undefined; } let winPS = useAlternateBitness ? this.alternateBitnessWinPS : this.winPS; if (winPS === undefined) { - const systemFolderPath: string = this.getSystem32Path({ useAlternateBitness }); + const systemFolderPath = this.getSystem32Path({ useAlternateBitness }); + + if (!systemFolderPath) { + return undefined; + } const winPSPath = path.join(systemFolderPath, "WindowsPowerShell", "v1.0", "powershell.exe"); @@ -427,7 +437,7 @@ export class PowerShellExeFinder { } private getProgramFilesPath( - { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { + { useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | undefined { if (!useAlternateBitness) { // Just use the native system bitness @@ -445,11 +455,15 @@ export class PowerShellExeFinder { } // We're a 32-bit process on 32-bit Windows, there is no other Program Files dir - return null; + return undefined; } - private getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | null { - const windir: string = process.env.windir; + private getSystem32Path({ useAlternateBitness = false }: { useAlternateBitness?: boolean } = {}): string | undefined { + const windir = process.env.windir; + + if (!windir) { + return undefined; + } if (!useAlternateBitness) { // Just use the native system bitness @@ -467,12 +481,14 @@ export class PowerShellExeFinder { } // We're on a 32-bit Windows, so no alternate bitness - return null; + return undefined; } } -export function getWindowsSystemPowerShellPath(systemFolderName: string) { - return path.join( +export function getWindowsSystemPowerShellPath(systemFolderName: string): string | undefined { + if (process.env.windir === undefined) { + return undefined; + } else return path.join( process.env.windir, systemFolderName, "WindowsPowerShell", @@ -495,6 +511,6 @@ class PossiblePowerShellExe implements IPossiblePowerShellExe { if (this.knownToExist === undefined) { this.knownToExist = await utils.checkIfFileExists(this.exePath); } - return this.knownToExist; + return this.knownToExist ?? false; } } diff --git a/src/process.ts b/src/process.ts index 3cf1d96d46..3a96a95dfd 100644 --- a/src/process.ts +++ b/src/process.ts @@ -21,8 +21,8 @@ export class PowerShellProcess { public onExited: vscode.Event; private onExitedEmitter = new vscode.EventEmitter(); - private consoleTerminal: vscode.Terminal = undefined; - private consoleCloseSubscription: vscode.Disposable; + private consoleTerminal?: vscode.Terminal; + private consoleCloseSubscription?: vscode.Disposable; constructor( public exePath: string, @@ -52,14 +52,14 @@ export class PowerShellProcess { this.startPsesArgs += `-LogPath '${PowerShellProcess.escapeSingleQuotes(editorServicesLogPath.fsPath)}' ` + - `-SessionDetailsPath '${PowerShellProcess.escapeSingleQuotes(this.sessionFilePath.fsPath)}' ` + + `-SessionDetailsPath '${PowerShellProcess.escapeSingleQuotes(this.sessionFilePath.fsPath)}' ` + `-FeatureFlags @(${featureFlags}) `; if (this.sessionSettings.integratedConsole.useLegacyReadLine) { this.startPsesArgs += "-UseLegacyReadLine"; } - const powerShellArgs = []; + const powerShellArgs: string[] = []; const useLoginShell: boolean = (utils.isMacOS && this.sessionSettings.startAsLoginShell.osx) @@ -132,7 +132,7 @@ export class PowerShellProcess { this.consoleCloseSubscription = vscode.window.onDidCloseTerminal((terminal) => this.onTerminalClose(terminal)); // Log that the PowerShell terminal process has been started - this.consoleTerminal.processId.then((pid) => this.logTerminalPid(pid, pwshName)); + this.consoleTerminal.processId.then((pid) => this.logTerminalPid(pid ?? 0, pwshName)); return sessionDetails; } @@ -143,18 +143,15 @@ export class PowerShellProcess { public dispose() { // Clean up the session file + this.log.write("Terminating PowerShell process..."); + PowerShellProcess.deleteSessionFile(this.sessionFilePath); - if (this.consoleCloseSubscription) { - this.consoleCloseSubscription.dispose(); - this.consoleCloseSubscription = undefined; - } + this.consoleCloseSubscription?.dispose(); + this.consoleCloseSubscription = undefined; - if (this.consoleTerminal) { - this.log.write("Terminating PowerShell process..."); - this.consoleTerminal.dispose(); - this.consoleTerminal = undefined; - } + this.consoleTerminal?.dispose(); + this.consoleTerminal = undefined; } public sendKeyPress() { @@ -162,7 +159,7 @@ export class PowerShellProcess { // because non-printing characters can cause havoc with different // languages and terminal settings. We discard the character server-side // anyway, so it doesn't matter what we send. - this.consoleTerminal.sendText("p", false); + this.consoleTerminal?.sendText("p", false); } private logTerminalPid(pid: number, exeName: string) { diff --git a/src/session.ts b/src/session.ts index b573ba17ab..cabae7415e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import net = require("net"); import path = require("path"); import * as semver from "semver"; @@ -83,40 +81,37 @@ export const PowerShellVersionRequestType = export class SessionManager implements Middleware { public HostName: string; public HostVersion: string; - public PowerShellExeDetails: IPowerShellExeDetails; + public PowerShellExeDetails: IPowerShellExeDetails | undefined; private ShowSessionMenuCommandName = "PowerShell.ShowSessionMenu"; - private editorServicesArgs: string; private sessionStatus: SessionStatus = SessionStatus.NeverStarted; - private suppressRestartPrompt: boolean; - private focusTerminalOnExecute: boolean; + private suppressRestartPrompt: boolean = false; private platformDetails: IPlatformDetails; private languageClientConsumers: LanguageClientConsumer[] = []; private languageStatusItem: vscode.LanguageStatusItem; - private languageServerProcess: PowerShellProcess; - private debugSessionProcess: PowerShellProcess; - private debugEventHandler: vscode.Disposable; - private versionDetails: IPowerShellVersionDetails; + private languageServerProcess: PowerShellProcess | undefined; + private debugSessionProcess: PowerShellProcess | undefined; + private debugEventHandler: vscode.Disposable | undefined; + private versionDetails: IPowerShellVersionDetails | undefined; private registeredHandlers: vscode.Disposable[] = []; private registeredCommands: vscode.Disposable[] = []; - private languageClient: LanguageClient = undefined; - private sessionSettings: Settings.ISettings = undefined; - private sessionDetails: IEditorServicesSessionDetails; + private languageClient: LanguageClient | undefined; + private sessionDetails: IEditorServicesSessionDetails | undefined; private sessionsFolder: vscode.Uri; - private bundledModulesPath: string; private starting: boolean = false; private started: boolean = false; - // Initialized by the start() method, since this requires settings - private powershellExeFinder: PowerShellExeFinder; - constructor( private extensionContext: vscode.ExtensionContext, + private sessionSettings: Settings.ISettings, private log: Logger, private documentSelector: DocumentSelector, hostName: string, - version: string, + hostVersion: string, private telemetryReporter: TelemetryReporter) { + // Create the language status item + this.languageStatusItem = this.createStatusBarItem(); + // Create a folder for the session files. this.sessionsFolder = vscode.Uri.joinPath(extensionContext.globalStorageUri, "sessions"); vscode.workspace.fs.createDirectory(this.sessionsFolder); @@ -124,7 +119,7 @@ export class SessionManager implements Middleware { this.platformDetails = getPlatformDetails(); this.HostName = hostName; - this.HostVersion = version; + this.HostVersion = hostVersion; const osBitness = this.platformDetails.isOS64Bit ? "64-bit" : "32-bit"; const procBitness = this.platformDetails.isProcess64Bit ? "64-bit" : "32-bit"; @@ -153,13 +148,14 @@ export class SessionManager implements Middleware { command.dispose(); } - this.languageClient.dispose(); + this.languageClient?.dispose(); } public setLanguageClientConsumers(languageClientConsumers: LanguageClientConsumer[]) { this.languageClientConsumers = languageClientConsumers; } + // The `exeNameOverride` is used by `restartSession` to override ANY other setting. public async start(exeNameOverride?: string) { // A simple lock because this function isn't re-entrant. if (this.started || this.starting) { @@ -167,125 +163,19 @@ export class SessionManager implements Middleware { } try { this.starting = true; - await this._start(exeNameOverride); + if (exeNameOverride) { + this.sessionSettings.powerShellDefaultVersion = exeNameOverride; + } + await this.log.startNewLog(this.sessionSettings.developer.editorServicesLogLevel); + await this.promptPowerShellExeSettingsCleanup(); + await this.migrateWhitespaceAroundPipeSetting(); + this.PowerShellExeDetails = await this.findPowerShell(); + this.languageServerProcess = await this.startPowerShell(); } finally { this.starting = false; } } - private async _start(exeNameOverride?: string) { - await Settings.validateCwdSetting(); - this.sessionSettings = Settings.load(); - - if (exeNameOverride) { - this.sessionSettings.powerShellDefaultVersion = exeNameOverride; - } - - await this.log.startNewLog(this.sessionSettings.developer.editorServicesLogLevel); - - // Create the PowerShell executable finder now - this.powershellExeFinder = new PowerShellExeFinder( - this.platformDetails, - this.sessionSettings.powerShellAdditionalExePaths); - - this.focusTerminalOnExecute = this.sessionSettings.integratedConsole.focusConsoleOnExecute; - - this.createStatusBarItem(); - - await this.promptPowerShellExeSettingsCleanup(); - - await this.migrateWhitespaceAroundPipeSetting(); - - try { - let powerShellExeDetails: IPowerShellExeDetails; - if (this.sessionSettings.powerShellDefaultVersion) { - for await (const details of this.powershellExeFinder.enumeratePowerShellInstallations()) { - // Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723 - const wantedName = this.sessionSettings.powerShellDefaultVersion; - if (wantedName.localeCompare(details.displayName, undefined, { sensitivity: "accent" }) === 0) { - powerShellExeDetails = details; - break; - } - } - } - this.PowerShellExeDetails = powerShellExeDetails || - await this.powershellExeFinder.getFirstAvailablePowerShellInstallation(); - } catch (e) { - this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`); - } - - this.suppressRestartPrompt = false; - - if (!this.PowerShellExeDetails) { - const message = "Unable to find PowerShell." - + " Do you have PowerShell installed?" - + " You can also configure custom PowerShell installations" - + " with the 'powershell.powerShellAdditionalExePaths' setting."; - - await this.log.writeAndShowErrorWithActions(message, [ - { - prompt: "Get PowerShell", - action: async () => { - const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode"); - vscode.env.openExternal(getPSUri); - }, - }, - ]); - return; - } - - this.bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath); - - if (this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { - const devBundledModulesPath = path.resolve(__dirname, this.sessionSettings.developer.bundledModulesPath); - - // Make sure the module's bin path exists - if (await utils.checkIfDirectoryExists(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { - this.bundledModulesPath = devBundledModulesPath; - } else { - this.log.write( - "\nWARNING: In development mode but PowerShellEditorServices dev module path cannot be " + - `found (or has not been built yet): ${devBundledModulesPath}\n`); - } - } - - this.editorServicesArgs = - `-HostName 'Visual Studio Code Host' ` + - `-HostProfileId 'Microsoft.VSCode' ` + - `-HostVersion '${this.HostVersion}' ` + - `-AdditionalModules @('PowerShellEditorServices.VSCode') ` + - `-BundledModulesPath '${PowerShellProcess.escapeSingleQuotes(this.bundledModulesPath)}' ` + - `-EnableConsoleRepl `; - - if (this.sessionSettings.integratedConsole.suppressStartupBanner) { - this.editorServicesArgs += "-StartupBanner '' "; - } else if (utils.isWindows && !this.PowerShellExeDetails.supportsProperArguments) { - // NOTE: On Windows we don't Base64 encode the startup command - // because it annoys some poorly implemented anti-virus scanners. - // Unfortunately this means that for some installs of PowerShell - // (such as through the `dotnet` package manager), we can't include - // a multi-line startup banner as the quotes break the command. - this.editorServicesArgs += `-StartupBanner '${this.HostName} Extension v${this.HostVersion}' `; - } else { - const startupBanner = `${this.HostName} Extension v${this.HostVersion} -Copyright (c) Microsoft Corporation. - -https://aka.ms/vscode-powershell -Type 'help' to get help. -`; - this.editorServicesArgs += `-StartupBanner "${startupBanner}" `; - } - - if (this.sessionSettings.developer.editorServicesWaitForDebugger) { - this.editorServicesArgs += "-WaitForDebugger "; - } - if (this.sessionSettings.developer.editorServicesLogLevel) { - this.editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; - } - - await this.startPowerShell(); - } - public async stop() { this.log.write("Shutting down language client..."); @@ -293,33 +183,28 @@ Type 'help' to get help. if (this.sessionStatus === SessionStatus.Failed) { // Before moving further, clear out the client and process if // the process is already dead (i.e. it crashed). - this.languageClient.dispose(); + this.languageClient?.dispose(); this.languageClient = undefined; - this.languageServerProcess.dispose(); + this.languageServerProcess?.dispose(); this.languageServerProcess = undefined; } this.sessionStatus = SessionStatus.Stopping; // Stop the language client. - if (this.languageClient !== undefined) { - await this.languageClient.stop(); - this.languageClient.dispose(); - this.languageClient = undefined; - } + await this.languageClient?.stop(); + this.languageClient?.dispose(); + this.languageClient = undefined; // Kill the PowerShell process(es) we spawned. - if (this.debugSessionProcess) { - this.debugSessionProcess.dispose(); - this.debugSessionProcess = undefined; - this.debugEventHandler.dispose(); - this.debugEventHandler = undefined; - } + this.debugSessionProcess?.dispose(); + this.debugSessionProcess = undefined; + this.debugEventHandler?.dispose(); + this.debugEventHandler = undefined; + + this.languageServerProcess?.dispose(); + this.languageServerProcess = undefined; - if (this.languageServerProcess) { - this.languageServerProcess.dispose(); - this.languageServerProcess = undefined; - } } finally { this.sessionStatus = SessionStatus.NotStarted; this.started = false; @@ -328,10 +213,15 @@ Type 'help' to get help. public async restartSession(exeNameOverride?: string) { await this.stop(); + + // Re-load and validate the settings. + await Settings.validateCwdSetting(); + this.sessionSettings = Settings.load(); + await this.start(exeNameOverride); } - public getSessionDetails(): IEditorServicesSessionDetails { + public getSessionDetails(): IEditorServicesSessionDetails | undefined { return this.sessionDetails; } @@ -339,7 +229,7 @@ Type 'help' to get help. return this.sessionStatus; } - public getPowerShellVersionDetails(): IPowerShellVersionDetails { + public getPowerShellVersionDetails(): IPowerShellVersionDetails | undefined { return this.versionDetails; } @@ -348,25 +238,33 @@ Type 'help' to get help. return vscode.Uri.joinPath(this.sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID + "-" + uniqueId + ".json"); } - public createDebugSessionProcess(sessionSettings: Settings.ISettings): PowerShellProcess { + public async createDebugSessionProcess(settings: Settings.ISettings): Promise { // NOTE: We only support one temporary Extension Terminal at a time. To // support more, we need to track each separately, and tie the session // for the event handler to the right process (and dispose of the event // handler when the process is disposed). - if (this.debugSessionProcess) { - this.debugSessionProcess.dispose() - this.debugEventHandler.dispose(); + this.debugSessionProcess?.dispose() + this.debugEventHandler?.dispose(); + + if (this.PowerShellExeDetails === undefined) { + return Promise.reject("Required PowerShellExeDetails undefined!") } + // TODO: It might not be totally necessary to update the session + // settings here, but I don't want to accidentally change this behavior + // just yet. Working on getting things to be more idempotent! + this.sessionSettings = settings; + + const bundledModulesPath = await this.getBundledModulesPath(); this.debugSessionProcess = new PowerShellProcess( this.PowerShellExeDetails.exePath, - this.bundledModulesPath, + bundledModulesPath, "[TEMP] PowerShell Extension", this.log, - this.editorServicesArgs + "-DebugServiceOnly ", + this.buildEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails) + "-DebugServiceOnly ", this.getNewSessionFilePath(), - sessionSettings); + this.sessionSettings); // Similar to the regular Extension Terminal, we need to send a key // press to the process spawned for temporary Extension Terminals when @@ -374,7 +272,7 @@ Type 'help' to get help. this.debugEventHandler = vscode.debug.onDidReceiveDebugSessionCustomEvent( e => { if (e.event === "powerShell/sendKeyPress") { - this.debugSessionProcess.sendKeyPress(); + this.debugSessionProcess?.sendKeyPress(); } } ); @@ -397,6 +295,10 @@ Type 'help' to get help. (codeLensToFix: vscode.CodeLens): vscode.CodeLens => { if (codeLensToFix.command?.command === "editor.action.showReferences") { const oldArgs = codeLensToFix.command.arguments; + if (oldArgs === undefined || oldArgs.length < 3) { + this.log.writeError("Code Lens arguments were malformed"); + return codeLensToFix; + } // Our JSON objects don't get handled correctly by // VS Code's built in editor.action.showReferences @@ -444,7 +346,7 @@ Type 'help' to get help. const newSetting = 'codeFormatting.addWhitespaceAroundPipe' const configurationTargetOfNewSetting = await Settings.getEffectiveConfigurationTarget(newSetting); const configurationTargetOfOldSetting = await Settings.getEffectiveConfigurationTarget(deprecatedSetting); - if (configurationTargetOfOldSetting !== null && configurationTargetOfNewSetting === null) { + if (configurationTargetOfOldSetting !== undefined && configurationTargetOfNewSetting === undefined) { const value = configuration.get(deprecatedSetting, configurationTargetOfOldSetting) await Settings.change(newSetting, value, configurationTargetOfOldSetting); await Settings.change(deprecatedSetting, undefined, configurationTargetOfOldSetting); @@ -452,47 +354,47 @@ Type 'help' to get help. } private async promptPowerShellExeSettingsCleanup() { - if (this.sessionSettings.powerShellExePath) { - let warningMessage = "The 'powerShell.powerShellExePath' setting is no longer used. "; - warningMessage += this.sessionSettings.powerShellDefaultVersion - ? "We can automatically remove it for you." - : "We can remove it from your settings and prompt you for which PowerShell you want to use."; + if (!this.sessionSettings.powerShellExePath) { // undefined or null + return; + } - const choice = await vscode.window.showWarningMessage(warningMessage, "Let's do it!"); + let warningMessage = "The 'powerShell.powerShellExePath' setting is no longer used. "; + warningMessage += this.sessionSettings.powerShellDefaultVersion + ? "We can automatically remove it for you." + : "We can remove it from your settings and prompt you for which PowerShell you want to use."; - if (choice === undefined) { - // They hit the 'x' to close the dialog. - return; - } + const choice = await vscode.window.showWarningMessage(warningMessage, "Let's do it!"); - this.suppressRestartPrompt = true; - try { - await Settings.change("powerShellExePath", undefined, true); - } finally { - this.suppressRestartPrompt = false; - } + if (choice === undefined) { + // They hit the 'x' to close the dialog. + return; + } - // Show the session menu at the end if they don't have a PowerShellDefaultVersion. - if (!this.sessionSettings.powerShellDefaultVersion) { - await vscode.commands.executeCommand(this.ShowSessionMenuCommandName); - } + this.suppressRestartPrompt = true; + try { + await Settings.change("powerShellExePath", undefined, true); + } finally { + this.suppressRestartPrompt = false; + } + + // Show the session menu at the end if they don't have a PowerShellDefaultVersion. + if (this.sessionSettings.powerShellDefaultVersion === undefined) { + await vscode.commands.executeCommand(this.ShowSessionMenuCommandName); } } private async onConfigurationUpdated() { const settings = Settings.load(); - this.focusTerminalOnExecute = settings.integratedConsole.focusConsoleOnExecute; - // Detect any setting changes that would affect the session if (!this.suppressRestartPrompt && (settings.cwd?.toLowerCase() !== this.sessionSettings.cwd?.toLowerCase() - || settings.powerShellDefaultVersion.toLowerCase() !== this.sessionSettings.powerShellDefaultVersion.toLowerCase() + || settings.powerShellDefaultVersion?.toLowerCase() !== this.sessionSettings.powerShellDefaultVersion?.toLowerCase() || settings.developer.editorServicesLogLevel.toLowerCase() !== this.sessionSettings.developer.editorServicesLogLevel.toLowerCase() || settings.developer.bundledModulesPath.toLowerCase() !== this.sessionSettings.developer.bundledModulesPath.toLowerCase() || settings.integratedConsole.useLegacyReadLine !== this.sessionSettings.integratedConsole.useLegacyReadLine || settings.integratedConsole.startInBackground !== this.sessionSettings.integratedConsole.startInBackground)) { - const response: string = await vscode.window.showInformationMessage( + const response = await vscode.window.showInformationMessage( "The PowerShell runtime configuration has changed, would you like to start a new session?", "Yes", "No"); @@ -511,26 +413,32 @@ Type 'help' to get help. "PowerShell.ShowSessionConsole", (isExecute?: boolean) => { this.showSessionTerminal(isExecute); }), vscode.commands.registerCommand( "PowerShell.WalkthroughTelemetry", (satisfaction: number) => { - this.sendTelemetryEvent("powershellWalkthroughSatisfaction", null, { level: satisfaction }); + this.sendTelemetryEvent("powershellWalkthroughSatisfaction", undefined, { level: satisfaction }); } ) ]; } - private async startPowerShell() { + private async startPowerShell(): Promise { + if (this.PowerShellExeDetails === undefined) { + await this.setSessionFailure("Unable to find PowerShell."); + return; + } + this.setSessionStatus("Starting...", SessionStatus.Initializing); - this.languageServerProcess = + const bundledModulesPath = await this.getBundledModulesPath(); + const languageServerProcess = new PowerShellProcess( this.PowerShellExeDetails.exePath, - this.bundledModulesPath, + bundledModulesPath, "PowerShell Extension", this.log, - this.editorServicesArgs, + this.buildEditorServicesArgs(bundledModulesPath, this.PowerShellExeDetails), this.getNewSessionFilePath(), this.sessionSettings); - this.languageServerProcess.onExited( + languageServerProcess.onExited( async () => { if (this.sessionStatus === SessionStatus.Running) { this.setSessionStatus("Session Exited!", SessionStatus.Failed); @@ -539,20 +447,19 @@ Type 'help' to get help. }); try { - this.sessionDetails = await this.languageServerProcess.start("EditorServices"); - } catch (error) { - this.log.write("PowerShell process failed to start."); - await this.setSessionFailure("PowerShell process failed to start: ", error); + this.sessionDetails = await languageServerProcess.start("EditorServices"); + } catch (err) { + await this.setSessionFailure("PowerShell process failed to start: ", err instanceof Error ? err.message : "unknown"); } - if (this.sessionDetails.status === "started") { + if (this.sessionDetails?.status === "started") { this.log.write("Language server started."); try { await this.startLanguageClient(this.sessionDetails); - } catch (error) { - await this.setSessionFailure("Language client failed to start: ", error); + } catch (err) { + await this.setSessionFailure("Language client failed to start: ", err instanceof Error ? err.message : "unknown"); } - } else if (this.sessionDetails.status === "failed") { + } else if (this.sessionDetails?.status === "failed") { if (this.sessionDetails.reason === "unsupported") { await this.setSessionFailure( "PowerShell language features are only supported on PowerShell version 5.1 and 7+. " + @@ -566,12 +473,116 @@ Type 'help' to get help. `PowerShell could not be started for an unknown reason '${this.sessionDetails.reason}'`); } } else { - // TODO: Handle other response cases + await this.setSessionFailure( + `Unknown session status '${this.sessionDetails?.status}' with reason '${this.sessionDetails?.reason}`); + } + + return languageServerProcess; + } + + private async findPowerShell(): Promise { + const powershellExeFinder = new PowerShellExeFinder( + this.platformDetails, + this.sessionSettings.powerShellAdditionalExePaths); + + let foundPowerShell: IPowerShellExeDetails | undefined; + try { + let defaultPowerShell: IPowerShellExeDetails | undefined; + if (this.sessionSettings.powerShellDefaultVersion !== undefined) { + for await (const details of powershellExeFinder.enumeratePowerShellInstallations()) { + // Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723 + const wantedName = this.sessionSettings.powerShellDefaultVersion; + if (wantedName.localeCompare(details.displayName, undefined, { sensitivity: "accent" }) === 0) { + defaultPowerShell = details; + break; + } + } + } + foundPowerShell = defaultPowerShell ?? await powershellExeFinder.getFirstAvailablePowerShellInstallation(); + } catch (e) { + this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`); + } + + if (foundPowerShell === undefined) { + const message = "Unable to find PowerShell." + + " Do you have PowerShell installed?" + + " You can also configure custom PowerShell installations" + + " with the 'powershell.powerShellAdditionalExePaths' setting."; + + await this.log.writeAndShowErrorWithActions(message, [ + { + prompt: "Get PowerShell", + action: async () => { + const getPSUri = vscode.Uri.parse("https://aka.ms/get-powershell-vscode"); + vscode.env.openExternal(getPSUri); + }, + }, + ]); + } + + return foundPowerShell; + } + + private async getBundledModulesPath(): Promise { + let bundledModulesPath = path.resolve(__dirname, this.sessionSettings.bundledModulesPath); + + if (this.extensionContext.extensionMode === vscode.ExtensionMode.Development) { + const devBundledModulesPath = path.resolve(__dirname, this.sessionSettings.developer.bundledModulesPath); + + // Make sure the module's bin path exists + if (await utils.checkIfDirectoryExists(path.join(devBundledModulesPath, "PowerShellEditorServices/bin"))) { + bundledModulesPath = devBundledModulesPath; + } else { + this.log.write( + "\nWARNING: In development mode but PowerShellEditorServices dev module path cannot be " + + `found (or has not been built yet): ${devBundledModulesPath}\n`); + } + } + + return bundledModulesPath; + } + + private buildEditorServicesArgs(bundledModulesPath: string, powerShellExeDetails: IPowerShellExeDetails): string { + let editorServicesArgs = + `-HostName 'Visual Studio Code Host' ` + + `-HostProfileId 'Microsoft.VSCode' ` + + `-HostVersion '${this.HostVersion}' ` + + `-AdditionalModules @('PowerShellEditorServices.VSCode') ` + + `-BundledModulesPath '${PowerShellProcess.escapeSingleQuotes(bundledModulesPath)}' ` + + `-EnableConsoleRepl `; + + if (this.sessionSettings.integratedConsole.suppressStartupBanner) { + editorServicesArgs += "-StartupBanner '' "; + } else if (utils.isWindows && !powerShellExeDetails.supportsProperArguments) { + // NOTE: On Windows we don't Base64 encode the startup command + // because it annoys some poorly implemented anti-virus scanners. + // Unfortunately this means that for some installs of PowerShell + // (such as through the `dotnet` package manager), we can't include + // a multi-line startup banner as the quotes break the command. + editorServicesArgs += `-StartupBanner '${this.HostName} Extension v${this.HostVersion}' `; + } else { + const startupBanner = `${this.HostName} Extension v${this.HostVersion} +Copyright (c) Microsoft Corporation. + +https://aka.ms/vscode-powershell +Type 'help' to get help. +`; + editorServicesArgs += `-StartupBanner "${startupBanner}" `; + } + + if (this.sessionSettings.developer.editorServicesWaitForDebugger) { + editorServicesArgs += "-WaitForDebugger "; + } + + if (this.sessionSettings.developer.editorServicesLogLevel) { + editorServicesArgs += `-LogLevel '${this.sessionSettings.developer.editorServicesLogLevel}' `; } + + return editorServicesArgs; } private async promptForRestart() { - const response: string = await vscode.window.showErrorMessage( + const response = await vscode.window.showErrorMessage( "The PowerShell Extension Terminal has stopped, would you like to restart it? IntelliSense and other features will not work without it!", "Yes", "No"); @@ -660,7 +671,7 @@ Type 'help' to get help. // press a key, thus mitigating all the quirk. this.languageClient.onNotification( SendKeyPressNotificationType, - () => { this.languageServerProcess.sendKeyPress(); }), + () => { this.languageServerProcess?.sendKeyPress(); }), this.languageClient.onNotification( ExecutionBusyStatusNotificationType, @@ -673,8 +684,8 @@ Type 'help' to get help. try { await this.languageClient.start(); - } catch (error) { - await this.setSessionFailure("Could not start language service: ", error); + } catch (err) { + await this.setSessionFailure("Could not start language service: ", err instanceof Error ? err.message : "unknown"); return; } @@ -695,39 +706,37 @@ Type 'help' to get help. return; } - const localVersion = semver.parse(this.versionDetails.version); - if (semver.lt(localVersion, "6.0.0")) { + const localVersion = semver.parse(this.versionDetails!.version); + if (semver.lt(localVersion!, "6.0.0")) { // Skip prompting when using Windows PowerShell for now. return; } try { // Fetch the latest PowerShell releases from GitHub. - const isPreRelease = !!semver.prerelease(localVersion); + const isPreRelease = !!semver.prerelease(localVersion!); const release: GitHubReleaseInformation = await GitHubReleaseInformation.FetchLatestRelease(isPreRelease); await InvokePowerShellUpdateCheck( this, - this.languageClient, - localVersion, - this.versionDetails.architecture, + this.languageClient!, + localVersion!, + this.versionDetails!.architecture, release); - } catch (error) { + } catch (err) { // Best effort. This probably failed to fetch the data from GitHub. - this.log.writeWarning(error.message); + this.log.writeWarning(err instanceof Error ? err.message : "unknown"); } } - private createStatusBarItem() { + private createStatusBarItem(): vscode.LanguageStatusItem { const statusTitle: string = "Show PowerShell Session Menu"; - if (this.languageStatusItem !== undefined) { - return; - } - this.languageStatusItem = vscode.languages.createLanguageStatusItem("powershell", this.documentSelector); - this.languageStatusItem.command = { title: statusTitle, command: this.ShowSessionMenuCommandName }; - this.languageStatusItem.text = "$(terminal-powershell)"; - this.languageStatusItem.detail = "PowerShell"; + const languageStatusItem = vscode.languages.createLanguageStatusItem("powershell", this.documentSelector); + languageStatusItem.command = { title: statusTitle, command: this.ShowSessionMenuCommandName }; + languageStatusItem.text = "$(terminal-powershell)"; + languageStatusItem.detail = "PowerShell"; + return languageStatusItem; } private setSessionStatus(statusText: string, status: SessionStatus): void { @@ -786,7 +795,11 @@ Type 'help' to get help. private async changePowerShellDefaultVersion(exePath: IPowerShellExeDetails) { this.suppressRestartPrompt = true; - await Settings.change("powerShellDefaultVersion", exePath.displayName, true); + try { + await Settings.change("powerShellDefaultVersion", exePath.displayName, true); + } finally { + this.suppressRestartPrompt = false; + } // We pass in the display name so that we force the extension to use that version // rather than pull from the settings. The issue we prevent here is when a @@ -798,19 +811,22 @@ Type 'help' to get help. // Shows the temp debug terminal if it exists, otherwise the session terminal. public showDebugTerminal(isExecute?: boolean) { if (this.debugSessionProcess) { - this.debugSessionProcess.showTerminal(isExecute && !this.focusTerminalOnExecute); + this.debugSessionProcess.showTerminal(isExecute && !this.sessionSettings.integratedConsole.focusConsoleOnExecute); } else { - this.languageServerProcess?.showTerminal(isExecute && !this.focusTerminalOnExecute) + this.languageServerProcess?.showTerminal(isExecute && !this.sessionSettings.integratedConsole.focusConsoleOnExecute) } } // Always shows the session terminal. public showSessionTerminal(isExecute?: boolean) { - this.languageServerProcess?.showTerminal(isExecute && !this.focusTerminalOnExecute); + this.languageServerProcess?.showTerminal(isExecute && !this.sessionSettings.integratedConsole.focusConsoleOnExecute); } private async showSessionMenu() { - const availablePowerShellExes = await this.powershellExeFinder.getAllAvailablePowerShellInstallations(); + const powershellExeFinder = new PowerShellExeFinder( + this.platformDetails, + this.sessionSettings.powerShellAdditionalExePaths); + const availablePowerShellExes = await powershellExeFinder.getAllAvailablePowerShellInstallations(); let sessionText: string; @@ -820,18 +836,22 @@ Type 'help' to get help. case SessionStatus.NotStarted: case SessionStatus.NeverStarted: case SessionStatus.Stopping: - const currentPowerShellExe = - availablePowerShellExes - .find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails.displayName.toLowerCase()); - - const powerShellSessionName = - currentPowerShellExe ? - currentPowerShellExe.displayName : - `PowerShell ${this.versionDetails.displayVersion} ` + - `(${this.versionDetails.architecture}) ${this.versionDetails.edition} Edition ` + - `[${this.versionDetails.version}]`; - - sessionText = `Current session: ${powerShellSessionName}`; + if (this.PowerShellExeDetails && this.versionDetails) { + const currentPowerShellExe = + availablePowerShellExes + .find((item) => item.displayName.toLowerCase() === this.PowerShellExeDetails!.displayName.toLowerCase()); + + const powerShellSessionName = + currentPowerShellExe ? + currentPowerShellExe.displayName : + `PowerShell ${this.versionDetails.displayVersion} ` + + `(${this.versionDetails.architecture}) ${this.versionDetails.edition} Edition ` + + `[${this.versionDetails.version}]`; + + sessionText = `Current session: ${powerShellSessionName}`; + } else { + sessionText = "Current session: Unknown"; + } break; case SessionStatus.Failed: @@ -844,7 +864,7 @@ Type 'help' to get help. const powerShellItems = availablePowerShellExes - .filter((item) => item.displayName !== this.PowerShellExeDetails.displayName) + .filter((item) => item.displayName !== this.PowerShellExeDetails?.displayName) .map((item) => { return new SessionMenuItem( `Switch to: ${item.displayName}`, @@ -864,7 +884,11 @@ Type 'help' to get help. async () => { // We pass in the display name so we guarantee that the session // will be the same PowerShell. - await this.restartSession(this.PowerShellExeDetails.displayName); + if (this.PowerShellExeDetails) { + await this.restartSession(this.PowerShellExeDetails.displayName); + } else { + await this.restartSession(); + } }), new SessionMenuItem( @@ -879,12 +903,12 @@ Type 'help' to get help. vscode .window .showQuickPick(menuItems) - .then((selectedItem) => { selectedItem.callback(); }); + .then((selectedItem) => { selectedItem?.callback(); }); } } class SessionMenuItem implements vscode.QuickPickItem { - public description: string; + public description: string | undefined; constructor( public readonly label: string, diff --git a/src/settings.ts b/src/settings.ts index 9d7687a3ae..b79b8fafa5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import vscode = require("vscode"); import utils = require("./utils"); import os = require("os"); @@ -73,10 +71,10 @@ export interface IDebuggingSettings { export interface IDeveloperSettings { featureFlags?: string[]; - bundledModulesPath?: string; - editorServicesLogLevel?: string; + bundledModulesPath: string; + editorServicesLogLevel: string; editorServicesWaitForDebugger?: boolean; - waitForSessionFileTimeoutSeconds?: number; + waitForSessionFileTimeoutSeconds: number; } export interface ISettings { @@ -85,20 +83,20 @@ export interface ISettings { // This setting is no longer used but is here to assist in cleaning up the users settings. powerShellExePath?: string; promptToUpdatePowerShell?: boolean; - bundledModulesPath?: string; - startAsLoginShell?: IStartAsLoginShellSettings; + bundledModulesPath: string; + startAsLoginShell: IStartAsLoginShellSettings; startAutomatically?: boolean; - enableProfileLoading?: boolean; + enableProfileLoading: boolean; helpCompletion: string; scriptAnalysis?: IScriptAnalysisSettings; - debugging?: IDebuggingSettings; - developer?: IDeveloperSettings; + debugging: IDebuggingSettings; + developer: IDeveloperSettings; codeFolding?: ICodeFoldingSettings; codeFormatting?: ICodeFormattingSettings; - integratedConsole?: IIntegratedConsoleSettings; - bugReporting?: IBugReportingSettings; - sideBar?: ISideBarSettings; - pester?: IPesterSettings; + integratedConsole: IIntegratedConsoleSettings; + bugReporting: IBugReportingSettings; + sideBar: ISideBarSettings; + pester: IPesterSettings; buttons?: IButtonSettings; cwd?: string; notebooks?: INotebooksSettings; @@ -107,27 +105,27 @@ export interface ISettings { } export interface IStartAsLoginShellSettings { - osx?: boolean; - linux?: boolean; + osx: boolean; + linux: boolean; } export interface IIntegratedConsoleSettings { showOnStartup?: boolean; startInBackground?: boolean; - focusConsoleOnExecute?: boolean; + focusConsoleOnExecute: boolean; useLegacyReadLine?: boolean; forceClearScrollbackBuffer?: boolean; suppressStartupBanner?: boolean; } export interface ISideBarSettings { - CommandExplorerVisibility?: boolean; + CommandExplorerVisibility: boolean; } export interface IPesterSettings { - useLegacyCodeLens?: boolean; - outputVerbosity?: string; - debugOutputVerbosity?: string; + useLegacyCodeLens: boolean; + outputVerbosity: string; + debugOutputVerbosity: string; } export interface IButtonSettings { @@ -226,15 +224,16 @@ export function load(): ISettings { saveMarkdownCellsAs: CommentType.BlockComment, }; + // TODO: I believe all the defaults can be removed, as the `package.json` should supply them (and be the source of truth). return { startAutomatically: configuration.get("startAutomatically", true), powerShellAdditionalExePaths: - configuration.get("powerShellAdditionalExePaths", undefined), + configuration.get("powerShellAdditionalExePaths"), powerShellDefaultVersion: - configuration.get("powerShellDefaultVersion", undefined), + configuration.get("powerShellDefaultVersion"), powerShellExePath: - configuration.get("powerShellExePath", undefined), + configuration.get("powerShellExePath"), promptToUpdatePowerShell: configuration.get("promptToUpdatePowerShell", true), bundledModulesPath: @@ -273,7 +272,7 @@ export function load(): ISettings { // the environment. See http://unix.stackexchange.com/a/119675/115410" configuration.get("startAsLoginShell", defaultStartAsLoginShellSettings), cwd: // NOTE: This must be validated at startup via `validateCwdSetting()`. There's probably a better way to do this. - configuration.get("cwd", undefined), + configuration.get("cwd"), enableReferencesCodeLens: configuration.get("enableReferencesCodeLens", true), analyzeOpenDocumentsOnly: @@ -282,21 +281,21 @@ export function load(): ISettings { } // Get the ConfigurationTarget (read: scope) of where the *effective* setting value comes from -export async function getEffectiveConfigurationTarget(settingName: string): Promise { +export async function getEffectiveConfigurationTarget(settingName: string): Promise { const configuration = vscode.workspace.getConfiguration(utils.PowerShellLanguageId); const detail = configuration.inspect(settingName); - let configurationTarget = null; - - if (typeof detail.workspaceFolderValue !== "undefined") { - configurationTarget = vscode.ConfigurationTarget.WorkspaceFolder; + if (detail === undefined) { + return undefined; + } else if (typeof detail.workspaceFolderValue !== "undefined") { + return vscode.ConfigurationTarget.WorkspaceFolder; } else if (typeof detail.workspaceValue !== "undefined") { - configurationTarget = vscode.ConfigurationTarget.Workspace; + return vscode.ConfigurationTarget.Workspace; } else if (typeof detail.globalValue !== "undefined") { - configurationTarget = vscode.ConfigurationTarget.Global; + return vscode.ConfigurationTarget.Global; } - return configurationTarget; + return undefined; } export async function change( @@ -328,10 +327,10 @@ function getWorkspaceSettingsWithDefaults( let hasPrompted: boolean = false; export async function validateCwdSetting(): Promise { - let cwd: string = vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get("cwd", undefined); + let cwd = vscode.workspace.getConfiguration(utils.PowerShellLanguageId).get("cwd"); // Only use the cwd setting if it exists. - if (await utils.checkIfDirectoryExists(cwd)) { + if (cwd !== undefined && await utils.checkIfDirectoryExists(cwd)) { return cwd; } @@ -340,7 +339,7 @@ export async function validateCwdSetting(): Promise { || vscode.workspace.workspaceFolders?.length === 0) { cwd = undefined; // If there is exactly one workspace folder, use that. - } if (vscode.workspace.workspaceFolders?.length === 1) { + } else if (vscode.workspace.workspaceFolders?.length === 1) { cwd = vscode.workspace.workspaceFolders?.[0].uri.fsPath; // If there is more than one workspace folder, prompt the user once. } else if (vscode.workspace.workspaceFolders?.length > 1 && !hasPrompted) { @@ -351,7 +350,7 @@ export async function validateCwdSetting(): Promise { cwd = (await vscode.window.showWorkspaceFolderPick(options))?.uri.fsPath; // Save the picked 'cwd' to the workspace settings. // We have to check again because the user may not have picked. - if (await utils.checkIfDirectoryExists(cwd)) { + if (cwd !== undefined && await utils.checkIfDirectoryExists(cwd)) { try { await change("cwd", cwd); } catch { diff --git a/src/utils.ts b/src/utils.ts index c6a259a741..279a41e1e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import os = require("os"); import path = require("path"); import vscode = require("vscode"); diff --git a/test/core/platform.test.ts b/test/core/platform.test.ts index 9e74722bf8..8ea72d9b21 100644 --- a/test/core/platform.test.ts +++ b/test/core/platform.test.ts @@ -44,7 +44,7 @@ interface ITestPlatform { name: string; platformDetails: platform.IPlatformDetails; filesystem: FileSystem.DirectoryItems; - environmentVars?: Record; + environmentVars: Record; } /** @@ -59,11 +59,12 @@ interface ITestPlatformSuccessCase extends ITestPlatform { // Platform configurations where we expect to find a set of PowerShells let successTestCases: ITestPlatformSuccessCase[]; -let msixAppDir = null; -let pwshMsixPath = null; -let pwshPreviewMsixPath = null; +let msixAppDir: string; +let pwshMsixPath: string; +let pwshPreviewMsixPath: string; + if (process.platform === "win32") { - msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps"); + msixAppDir = path.join(process.env.LOCALAPPDATA!, "Microsoft", "WindowsApps"); pwshMsixPath = path.join(msixAppDir, "Microsoft.PowerShell_8wekyb3d8bbwe", "pwsh.exe"); pwshPreviewMsixPath = path.join(msixAppDir, "Microsoft.PowerShellPreview_8wekyb3d8bbwe", "pwsh.exe"); @@ -441,6 +442,7 @@ if (process.platform === "win32") { isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, expectedPowerShellSequence: [ { exePath: "/usr/bin/pwsh", @@ -481,6 +483,7 @@ if (process.platform === "win32") { isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, expectedPowerShellSequence: [ { exePath: "/usr/local/bin/pwsh", @@ -507,6 +510,7 @@ if (process.platform === "win32") { isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, expectedPowerShellSequence: [ { exePath: "/usr/bin/pwsh", @@ -527,6 +531,7 @@ if (process.platform === "win32") { isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, expectedPowerShellSequence: [ { exePath: "/snap/bin/pwsh", @@ -547,6 +552,7 @@ if (process.platform === "win32") { isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, expectedPowerShellSequence: [ { exePath: "/usr/local/bin/pwsh", @@ -619,6 +625,7 @@ const errorTestCases: ITestPlatform[] = [ isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, filesystem: {}, }, { @@ -628,6 +635,7 @@ const errorTestCases: ITestPlatform[] = [ isOS64Bit: true, isProcess64Bit: true, }, + environmentVars: {}, filesystem: {}, }, ]; @@ -635,10 +643,8 @@ const errorTestCases: ITestPlatform[] = [ function setupTestEnvironment(testPlatform: ITestPlatform) { mockFS(testPlatform.filesystem); - if (testPlatform.environmentVars) { - for (const envVar of Object.keys(testPlatform.environmentVars)) { - sinon.stub(process.env, envVar).value(testPlatform.environmentVars[envVar]); - } + for (const envVar of Object.keys(testPlatform.environmentVars)) { + sinon.stub(process.env, envVar).value(testPlatform.environmentVars[envVar]); } } @@ -711,9 +717,9 @@ describe("Platform module", function () { const defaultPowerShell = await powerShellExeFinder.getFirstAvailablePowerShellInstallation(); const expectedPowerShell = testPlatform.expectedPowerShellSequence[0]; - assert.strictEqual(defaultPowerShell.exePath, expectedPowerShell.exePath); - assert.strictEqual(defaultPowerShell.displayName, expectedPowerShell.displayName); - assert.strictEqual(defaultPowerShell.supportsProperArguments, expectedPowerShell.supportsProperArguments); + assert.strictEqual(defaultPowerShell.exePath, expectedPowerShell!.exePath); + assert.strictEqual(defaultPowerShell.displayName, expectedPowerShell!.displayName); + assert.strictEqual(defaultPowerShell.supportsProperArguments, expectedPowerShell!.supportsProperArguments); }); } @@ -747,9 +753,9 @@ describe("Platform module", function () { const foundPowerShell = foundPowerShells[i]; const expectedPowerShell = testPlatform.expectedPowerShellSequence[i]; - assert.strictEqual(foundPowerShell && foundPowerShell.exePath, expectedPowerShell.exePath); - assert.strictEqual(foundPowerShell && foundPowerShell.displayName, expectedPowerShell.displayName); - assert.strictEqual(foundPowerShell && foundPowerShell.supportsProperArguments, expectedPowerShell.supportsProperArguments); + assert.strictEqual(foundPowerShell && foundPowerShell.exePath, expectedPowerShell!.exePath); + assert.strictEqual(foundPowerShell && foundPowerShell.displayName, expectedPowerShell!.displayName); + assert.strictEqual(foundPowerShell && foundPowerShell.supportsProperArguments, expectedPowerShell!.supportsProperArguments); } assert.strictEqual( @@ -785,7 +791,7 @@ describe("Platform module", function () { function getWinPSPath(systemDir: string) { return path.join( - testPlatform.environmentVars.windir, + testPlatform.environmentVars.windir!, systemDir, "WindowsPowerShell", "v1.0", diff --git a/test/core/settings.test.ts b/test/core/settings.test.ts index 1143f8333f..687f7aa906 100644 --- a/test/core/settings.test.ts +++ b/test/core/settings.test.ts @@ -31,7 +31,7 @@ describe("Settings module", function () { it("Doesn't throw when updating at user-level", async function () { await Settings.change("powerShellAdditionalExePaths", psExeDetails, true /* user-level */); - const result = Settings.load().powerShellAdditionalExePaths["My PowerShell"]; + const result = Settings.load().powerShellAdditionalExePaths!["My PowerShell"]; assert.notStrictEqual(result, undefined); assert.strictEqual(result, psExeDetails["My PowerShell"]); }); @@ -44,6 +44,6 @@ describe("Settings module", function () { await Settings.change("helpCompletion", undefined, false); target = await Settings.getEffectiveConfigurationTarget("helpCompletion"); - assert.strictEqual(target, null); + assert.strictEqual(target, undefined); }); }); diff --git a/test/features/ISECompatibility.test.ts b/test/features/ISECompatibility.test.ts index 64782db7ed..5531dfc3bc 100644 --- a/test/features/ISECompatibility.test.ts +++ b/test/features/ISECompatibility.test.ts @@ -7,7 +7,7 @@ import { ISECompatibilityFeature } from "../../src/features/ISECompatibility"; import utils = require("../utils"); describe("ISE compatibility feature", function () { - let currentTheme: string; + let currentTheme: string | undefined; async function enableISEMode() { await vscode.commands.executeCommand("PowerShell.EnableISEMode"); } async function disableISEMode() { await vscode.commands.executeCommand("PowerShell.DisableISEMode"); } diff --git a/test/utils.ts b/test/utils.ts index b47c113e9c..d31212a7b2 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -"use strict"; - import * as path from "path"; import * as vscode from "vscode"; import { IPowerShellExtensionClient } from "../src/features/ExternalApi"; @@ -16,7 +14,7 @@ export const extensionId = `${packageJSON.publisher}.${packageJSON.name}`; export async function ensureExtensionIsActivated(): Promise { const extension = vscode.extensions.getExtension(extensionId); - if (!extension.isActive) { await extension.activate(); } + if (!extension!.isActive) { await extension!.activate(); } return extension!.exports as IPowerShellExtensionClient; } diff --git a/tsconfig.json b/tsconfig.json index beb738d4ad..f0ad892c55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,11 +11,12 @@ ], "sourceMap": true, "rootDir": ".", - // TODO: We need to enable stricter checking... - // "strict": true, - // "noImplicitReturns": true, - // "noFallthroughCasesInSwitch": true, - // "noUnusedParameters": true + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, }, "include": [ "src", "test" ], }