diff --git a/examples/PromptExamples.ps1 b/examples/PromptExamples.ps1 new file mode 100644 index 0000000000..959c5c1a16 --- /dev/null +++ b/examples/PromptExamples.ps1 @@ -0,0 +1,10 @@ + +# Multi-choice prompt +$choices = @( + New-Object "System.Management.Automation.Host.ChoiceDescription" "&Apple", "Apple" + New-Object "System.Management.Automation.Host.ChoiceDescription" "&Banana", "Banana" + New-Object "System.Management.Automation.Host.ChoiceDescription" "&Orange", "Orange" +) + +$defaults = [int[]]@(0, 2) +$host.UI.PromptForChoice("Choose a fruit", "You may choose more than one", $choices, $defaults) \ No newline at end of file diff --git a/package.json b/package.json index 207bb015ed..ae4b16ae73 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ }, "main": "./out/main", "activationEvents": [ - "onLanguage:powershell" + "onLanguage:powershell", + "onCommand:PowerShell.NewProjectFromTemplate" ], "dependencies": { "vscode-languageclient": "1.3.1" @@ -128,6 +129,16 @@ "command": "PowerShell.SelectPSSARules", "title": "Select PSScriptAnalyzer Rules", "category": "PowerShell" + }, + { + "command": "PowerShell.ShowSessionOutput", + "title": "Show Session Output", + "category": "PowerShell" + }, + { + "command": "PowerShell.NewProjectFromTemplate", + "title": "Create New Project from Plaster Template", + "category": "PowerShell" } ], "snippets": [ diff --git a/src/checkboxQuickPick.ts b/src/checkboxQuickPick.ts index c3d579d6ba..3babd238ad 100644 --- a/src/checkboxQuickPick.ts +++ b/src/checkboxQuickPick.ts @@ -3,74 +3,97 @@ *--------------------------------------------------------*/ import vscode = require("vscode"); -import QuickPickItem = vscode.QuickPickItem; -export class CheckboxQuickPickItem { - name: string; +var confirmItemLabel: string = "$(checklist) Confirm"; +var checkedPrefix: string = "[ $(check) ]"; +var uncheckedPrefix: string = "[ ]"; +var defaultPlaceHolder: string = "Select 'Confirm' to confirm or press 'Esc' key to cancel"; + +export interface CheckboxQuickPickItem { + label: string; + description?: string; isSelected: boolean; } -export class CheckboxQuickPick { - private static readonly confirm: string = "$(check)"; - private static readonly checkboxOn: string = "[ x ]"; - private static readonly checkboxOff: string = "[ ]"; - private static readonly confirmPlaceHolder: string = "Select 'Confirm' to confirm change; Press 'esc' key to cancel changes"; - - public static show( - checkboxQuickPickItems: CheckboxQuickPickItem[], - callback: (items: CheckboxQuickPickItem[]) => void): void { - CheckboxQuickPick.showInner(checkboxQuickPickItems.slice(), callback); - } - - private static showInner( - items: CheckboxQuickPickItem[], - callback: (items: CheckboxQuickPickItem[]) => void): void { +export interface CheckboxQuickPickOptions { + confirmPlaceHolder: string; +} + +var defaultOptions:CheckboxQuickPickOptions = { confirmPlaceHolder: defaultPlaceHolder}; + +export function showCheckboxQuickPick( + items: CheckboxQuickPickItem[], + options: CheckboxQuickPickOptions = 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; + }) +} + +function getQuickPickItems(items: CheckboxQuickPickItem[]): vscode.QuickPickItem[] { + + let quickPickItems: vscode.QuickPickItem[] = []; + quickPickItems.push({ label: confirmItemLabel, description: "" }); + + items.forEach(item => + quickPickItems.push({ + label: convertToCheckBox(item), + description: item.description + })); + + return quickPickItems; +} + +function showInner( + items: CheckboxQuickPickItem[], + options: CheckboxQuickPickOptions): Thenable { + + var quickPickThenable: Thenable = vscode.window.showQuickPick( - CheckboxQuickPick.getQuickPickItems(items), + getQuickPickItems(items), { ignoreFocusOut: true, matchOnDescription: true, - placeHolder: CheckboxQuickPick.confirmPlaceHolder - }).then((selection) => { - if (!selection) { - return; - } - - if (selection.label === CheckboxQuickPick.confirm) { - callback(items); - return; - } - - let index: number = CheckboxQuickPick.getRuleIndex(items, selection.description); - CheckboxQuickPick.toggleSelection(items[index]); - CheckboxQuickPick.showInner(items, callback); + placeHolder: options.confirmPlaceHolder }); - } - - private static getRuleIndex(items: CheckboxQuickPickItem[], itemLabel: string): number { - return items.findIndex(item => item.name === itemLabel); - } - - private static getQuickPickItems(items: CheckboxQuickPickItem[]): QuickPickItem[] { - let quickPickItems: QuickPickItem[] = []; - quickPickItems.push({ label: CheckboxQuickPick.confirm, description: "Confirm" }); - items.forEach(item => - quickPickItems.push({ - label: CheckboxQuickPick.convertToCheckBox(item.isSelected), description: item.name - })); - return quickPickItems; - } - - private static toggleSelection(item: CheckboxQuickPickItem): void { - item.isSelected = !item.isSelected; - } - - private static convertToCheckBox(state: boolean): string { - if (state) { - return CheckboxQuickPick.checkboxOn; - } - else { - return CheckboxQuickPick.checkboxOff; - } - } + + return quickPickThenable.then( + (selection) => { + if (!selection) { + //return Promise.reject("showCheckBoxQuickPick cancelled") + return Promise.resolve(undefined); + } + + if (selection.label === confirmItemLabel) { + return selection; + } + + let index: number = getItemIndex(items, selection.label); + + if (index >= 0) { + toggleSelection(items[index]); + } + else { + console.log(`Couldn't find CheckboxQuickPickItem for label '${selection.label}'`); + } + + return showInner(items, options); + }); +} + +function getItemIndex(items: CheckboxQuickPickItem[], itemLabel: string): number { + var trimmedLabel = itemLabel.substr(itemLabel.indexOf("]") + 2); + return items.findIndex(item => item.label === trimmedLabel); +} + +function toggleSelection(item: CheckboxQuickPickItem): void { + item.isSelected = !item.isSelected; +} + +function convertToCheckBox(item: CheckboxQuickPickItem): string { + return `${item.isSelected ? checkedPrefix : uncheckedPrefix} ${item.label}`; } \ No newline at end of file diff --git a/src/features/Console.ts b/src/features/Console.ts index 2c6c111846..58ab32a654 100644 --- a/src/features/Console.ts +++ b/src/features/Console.ts @@ -4,6 +4,7 @@ import vscode = require('vscode'); import { IFeature } from '../feature'; +import { showCheckboxQuickPick, CheckboxQuickPickItem } from '../checkboxQuickPick' import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; export namespace EvaluateRequest { @@ -46,14 +47,15 @@ interface ShowInputPromptRequestArgs { } interface ShowChoicePromptRequestArgs { + isMultiChoice: boolean; caption: string; message: string; choices: ChoiceDetails[]; - defaultChoice: number; + defaultChoices: number[]; } interface ShowChoicePromptResponseBody { - chosenItem: string; + responseText: string; promptCancelled: boolean; } @@ -66,36 +68,62 @@ function showChoicePrompt( promptDetails: ShowChoicePromptRequestArgs, client: LanguageClient) : Thenable { - var quickPickItems = - promptDetails.choices.map(choice => { - return { - label: choice.label, - description: choice.helpMessage - } - }); + var resultThenable: Thenable = undefined; - // Shift the default item to the front of the - // array so that the user can select it easily - if (promptDetails.defaultChoice > -1 && - promptDetails.defaultChoice < promptDetails.choices.length) { + if (!promptDetails.isMultiChoice) { + var quickPickItems = + promptDetails.choices.map(choice => { + return { + label: choice.label, + description: choice.helpMessage + } + }); - var defaultChoiceItem = quickPickItems[promptDetails.defaultChoice]; - quickPickItems.splice(promptDetails.defaultChoice, 1); + if (promptDetails.defaultChoices && + promptDetails.defaultChoices.length > 0) { + + // Shift the default items to the front of the + // array so that the user can select it easily + var defaultChoice = promptDetails.defaultChoices[0]; + if (defaultChoice > -1 && + defaultChoice < promptDetails.choices.length) { + + var defaultChoiceItem = quickPickItems[defaultChoice]; + quickPickItems.splice(defaultChoice, 1); + + // Add the default choice to the head of the array + quickPickItems = [defaultChoiceItem].concat(quickPickItems); + } + } - // Add the default choice to the head of the array - quickPickItems = [defaultChoiceItem].concat(quickPickItems); + resultThenable = + vscode.window + .showQuickPick( + quickPickItems, + { placeHolder: promptDetails.caption + " - " + promptDetails.message }) + .then(onItemSelected); } + else { + var checkboxQuickPickItems = + promptDetails.choices.map(choice => { + return { + label: choice.label, + description: choice.helpMessage, + isSelected: false + } + }); - // For some bizarre reason, the quick pick dialog does not - // work if I return the Thenable immediately at this point. - // It only works if I save the thenable to a variable and - // return the variable instead... - var resultThenable = - vscode.window - .showQuickPick( - quickPickItems, - { placeHolder: promptDetails.caption + " - " + promptDetails.message }) - .then(onItemSelected); + // Select the defaults + promptDetails.defaultChoices.forEach(choiceIndex => { + checkboxQuickPickItems[choiceIndex].isSelected = true + }); + + resultThenable = + showCheckboxQuickPick( + checkboxQuickPickItems, + { confirmPlaceHolder: `${promptDetails.caption} - ${promptDetails.message}`}) + .then(onItemsSelected); + } return resultThenable; } @@ -112,18 +140,34 @@ function showInputPrompt( return resultThenable; } +function onItemsSelected(chosenItems: CheckboxQuickPickItem[]): ShowChoicePromptResponseBody { + if (chosenItems !== undefined) { + return { + promptCancelled: false, + responseText: chosenItems.filter(item => item.isSelected).map(item => item.label).join(", ") + }; + } + else { + // User cancelled the prompt, send the cancellation + return { + promptCancelled: true, + responseText: undefined + }; + } +} + function onItemSelected(chosenItem: vscode.QuickPickItem): ShowChoicePromptResponseBody { if (chosenItem !== undefined) { return { promptCancelled: false, - chosenItem: chosenItem.label + responseText: chosenItem.label }; } else { // User cancelled the prompt, send the cancellation return { promptCancelled: true, - chosenItem: undefined + responseText: undefined }; } } @@ -144,12 +188,12 @@ function onInputEntered(responseText: string): ShowInputPromptResponseBody { } export class ConsoleFeature implements IFeature { - private command: vscode.Disposable; + private commands: vscode.Disposable[]; private languageClient: LanguageClient; private consoleChannel: vscode.OutputChannel; constructor() { - this.command = + this.commands = [ vscode.commands.registerCommand('PowerShell.RunSelection', () => { if (this.languageClient === undefined) { // TODO: Log error message @@ -175,7 +219,13 @@ export class ConsoleFeature implements IFeature { // Show the output window if it isn't already visible this.consoleChannel.show(vscode.ViewColumn.Three); - }); + }), + + vscode.commands.registerCommand('PowerShell.ShowSessionOutput', () => { + // Show the output window if it isn't already visible + this.consoleChannel.show(vscode.ViewColumn.Three); + }) + ]; this.consoleChannel = vscode.window.createOutputChannel("PowerShell Output"); } @@ -197,7 +247,7 @@ export class ConsoleFeature implements IFeature { } public dispose() { - this.command.dispose(); + this.commands.forEach(command => command.dispose()); this.consoleChannel.dispose(); } } diff --git a/src/features/NewFileOrProject.ts b/src/features/NewFileOrProject.ts new file mode 100644 index 0000000000..6a91938622 --- /dev/null +++ b/src/features/NewFileOrProject.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import vscode = require('vscode'); +import { IFeature } from '../feature'; +import { LanguageClient, RequestType, NotificationType } from 'vscode-languageclient'; + +export class NewFileOrProjectFeature implements IFeature { + + private readonly loadIcon = " $(sync) "; + private command: vscode.Disposable; + private languageClient: LanguageClient; + private waitingForClientToken: vscode.CancellationTokenSource; + + constructor() { + this.command = + vscode.commands.registerCommand('PowerShell.NewProjectFromTemplate', () => { + + if (!this.languageClient && !this.waitingForClientToken) { + + // If PowerShell isn't finished loading yet, show a loading message + // until the LanguageClient is passed on to us + this.waitingForClientToken = new vscode.CancellationTokenSource(); + vscode.window + .showQuickPick( + ["Cancel"], + { placeHolder: "New Project: Please wait, starting PowerShell..." }, + this.waitingForClientToken.token) + .then(response => { if (response === "Cancel") { this.clearWaitingToken(); } }); + + // Cancel the loading prompt after 60 seconds + setTimeout(() => { + if (this.waitingForClientToken) { + this.clearWaitingToken(); + + vscode.window.showErrorMessage( + "New Project: PowerShell session took too long to start."); + } + }, 60000); + } + else { + this.showProjectTemplates(); + } + }); + } + + public setLanguageClient(languageClient: LanguageClient) { + this.languageClient = languageClient; + + if (this.waitingForClientToken) { + this.clearWaitingToken(); + this.showProjectTemplates(); + } + } + + public dispose() { + this.command.dispose(); + } + + 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 getProjectTemplates(includeInstalledModules: boolean): Thenable { + return this.languageClient + .sendRequest( + GetProjectTemplatesRequest.type, + { includeInstalledModules: 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 { + var templates = response.templates.map( + template => { + return { + label: template.title, + description: `v${template.version} by ${template.author}, tags: ${template.tags}`, + detail: template.description, + template: template + } + }); + + if (!includeInstalledModules) { + templates = + [{ label: this.loadIcon, description: "Load additional templates from installed modules", template: undefined }] + .concat(templates) + } + else { + templates = + [{ label: this.loadIcon, description: "Refresh template list", template: undefined }] + .concat(templates) + } + + return templates; + } + }); + } + + private createProjectFromTemplate(template: TemplateDetails): 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( + NewProjectFromTemplateRequest.type, + { templatePath: template.templatePath, destinationPath: 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 openWorkspacePath(workspacePath: string) { + // Open the created project in a new window + vscode.commands.executeCommand( + "vscode.openFolder", + vscode.Uri.file(workspacePath), + true); + } + + private clearWaitingToken() { + if (this.waitingForClientToken) { + this.waitingForClientToken.dispose(); + this.waitingForClientToken = undefined; + } + } +} + +interface TemplateQuickPickItem extends vscode.QuickPickItem { + template: TemplateDetails +} + +interface TemplateDetails { + title: string; + version: string; + author: string; + description: string; + tags: string; + templatePath: string; +} + +namespace GetProjectTemplatesRequest { + export const type: RequestType = + { get method() { return 'powerShell/getProjectTemplates'; } }; +} + +interface GetProjectTemplatesRequestArgs { + includeInstalledModules: boolean; +} + +interface GetProjectTemplatesResponseBody { + needsModuleInstall: boolean; + templates: TemplateDetails[]; +} + +namespace NewProjectFromTemplateRequest { + export const type: RequestType = + { get method() { return 'powerShell/newProjectFromTemplate'; } }; +} + +interface NewProjectFromTemplateRequestArgs { + destinationPath: string; + templatePath: string; +} + +interface NewProjectFromTemplateResponseBody { + creationSuccessful: boolean; +} \ No newline at end of file diff --git a/src/features/SelectPSSARules.ts b/src/features/SelectPSSARules.ts index c3908e5556..aece6f16ab 100644 --- a/src/features/SelectPSSARules.ts +++ b/src/features/SelectPSSARules.ts @@ -5,7 +5,7 @@ import vscode = require("vscode"); import { IFeature } from "../feature"; import { LanguageClient, RequestType } from "vscode-languageclient"; -import { CheckboxQuickPickItem, CheckboxQuickPick } from "../checkboxQuickPick"; +import { CheckboxQuickPickItem, showCheckboxQuickPick } from "../checkboxQuickPick"; export namespace GetPSSARulesRequest { export const type: RequestType = { get method(): string { return "powerShell/getPSSARules"; } }; @@ -43,16 +43,18 @@ export class SelectPSSARulesFeature implements IFeature { return; } let options: CheckboxQuickPickItem[] = returnedRules.map(function (rule: RuleInfo): CheckboxQuickPickItem { - return { name: rule.name, isSelected: rule.isEnabled }; + return { label: rule.name, isSelected: rule.isEnabled }; }); - CheckboxQuickPick.show(options, (updatedOptions) => { - let filepath: string = vscode.window.activeTextEditor.document.uri.fsPath; - let ruleInfos: RuleInfo[] = updatedOptions.map( - function (option: CheckboxQuickPickItem): RuleInfo { - return { name: option.name, isEnabled: option.isSelected }; - }); - let requestParams: SetPSSARulesRequestParams = {filepath, ruleInfos}; - this.languageClient.sendRequest(SetPSSARulesRequest.type, requestParams); + + showCheckboxQuickPick(options) + .then(updatedOptions => { + let filepath: string = vscode.window.activeTextEditor.document.uri.fsPath; + let ruleInfos: RuleInfo[] = updatedOptions.map( + function (option: CheckboxQuickPickItem): RuleInfo { + return { name: option.label, isEnabled: option.isSelected }; + }); + let requestParams: SetPSSARulesRequestParams = {filepath, ruleInfos}; + this.languageClient.sendRequest(SetPSSARulesRequest.type, requestParams); }); }); }); diff --git a/src/main.ts b/src/main.ts index 466e559260..644b691018 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { SessionManager } from './session'; import { PowerShellLanguageId } from './utils'; import { ConsoleFeature } from './features/Console'; import { OpenInISEFeature } from './features/OpenInISE'; +import { NewFileOrProjectFeature } from './features/NewFileOrProject'; import { ExpandAliasFeature } from './features/ExpandAlias'; import { ShowHelpFeature } from './features/ShowOnlineHelp'; import { FindModuleFeature } from './features/PowerShellFindModule'; @@ -93,7 +94,8 @@ export function activate(context: vscode.ExtensionContext): void { new FindModuleFeature(), new ExtensionCommandsFeature(), new SelectPSSARulesFeature(), - new CodeActionsFeature() + new CodeActionsFeature(), + new NewFileOrProjectFeature() ]; sessionManager = @@ -106,9 +108,6 @@ export function activate(context: vscode.ExtensionContext): void { } export function deactivate(): void { - // Finish the logger - logger.dispose(); - // Clean up all extension features extensionFeatures.forEach(feature => { feature.dispose(); @@ -116,4 +115,7 @@ export function deactivate(): void { // Dispose of the current session sessionManager.dispose(); + + // Dispose of the logger + logger.dispose(); }