Add spell checking to PowerShell ISE

4126262859_88a806a90e_m

While the PowerShell ISE is obviously for writing PowerShell code rather than text, I usually have a fair amount of it in some of my scripts due to comments or hard coded text elements. Therefore, I thought that it would be handy to have the ability to verify selected text against a spell checker.

spellcheck

I have extended my ISEUtils (It can be downloaded from my GitHub repository) module with the functionality. The spell checking works against the selected text within the ISE and can be activated by pressing F7.
The spell checking is based on the built-in ability of the WPF TextBox which automatically adds the squiggly lines for misspelled words and the spelling suggestions within the context menu. Clicking on the button “Auto Correct” will automatically correct the whole text using the first spelling suggestion (this can lead to some funny results). The spelling uses the “en-us” dictionary if you want to change this you will need to modify the code.
The “spell-checking code” is fairly simple (using ShowUI to build the GUI):

New-DockPanel {
        New-Button 'Auto-Correct (use first spelling suggestion)' -Dock Bottom -On_Click{
            $txtBox = $this.Parent.Children | where {$_.Name -eq 'txtBox'}
            $startIndex = 0
            $errorIndex = $txtBox.GetNextSpellingErrorCharacterIndex($startIndex, [System.Windows.Documents.LogicalDirection]::Forward)
            while ($errorIndex -ne -1){
                $startIndex = $errorIndex
                $error = $txtBox.GetSpellingError($errorIndex)
                $suggestion = @($error.suggestions)[0]
                if ($suggestion){
                    $error.Correct($suggestion)
                }
                $errorIndex = $txtBox.GetNextSpellingErrorCharacterIndex($startIndex, [System.Windows.Documents.LogicalDirection]::Forward)
                if ($errorIndex -eq $startIndex){
                    $errorIndex = $txtBox.Text.IndexOf(' ',$startIndex) + 1
                }
            }
        }
        New-TextBox -Language 'en-us' -Dock Top -TextWrapping Wrap -VerticalAlignment Stretch -HorizontalAlignment Stretch -Name txtBox  -FontSize 15 -AcceptsReturn -On_Loaded {                       
            $this.Text = $psise.CurrentPowerShellTab.Files.SelectedFile.Editor.SelectedText           
            $this.SpellCheck.IsEnabled = $true          
        }
} -Show

shareThoughts


Photo Credit: Will Montague via Compfight cc

Zen Coding for the PowerShell console and ISE

8381892061_eb3babe76a_m
First of all let’s clarify what Zen Coding actually is. According to their website: Emmet (formerly known as Zen Coding) is …

… a web-developer’s toolkit that can greatly improve your HTML & CSS workflow.

But what does this have to do with PowerShell? At least I find myself quite often trying to convert PowerShell output into HTML or even using the text manipulation capabilities of PowerShell to dynamically construct some static web content. Yes, I hear you shouting already isn’t that why we have ConvertTo-HTML and Here-Strings (and here)? Granted that those can make the job already pretty easy (I would definitely recommend you to have a look into Creating HTML Reports in PowerShell if you haven’t yet), but there is still an even better way to (dynamically) generate static HTML pages from within PowerShell. Before looking into the implementation details let’s have a look at some examples on what zen coding looks like (considering we would have a PowerShell function called zenCode that expands zen code expressions):

zenCode 'html:5'
<# output:
<!DOCTYPE html []>
<html lang="en">
	<head>
		<title></title>
		<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
	</head>
	<body></body>
</html>
#>
zencode 'ul.generic-list>(li.item>lorem10)*4'
<# output:
<ul class="generic-list">
	<li class="item">Sapien elit in malesuada semper mi, id sollicitudin urna fermentum.</li>
	<li class="item">Sapien elit in malesuada semper mi, id sollicitudin urna fermentum.</li>
	<li class="item">Sapien elit in malesuada semper mi, id sollicitudin urna fermentum.</li>
	<li class="item">Sapien elit in malesuada semper mi, id sollicitudin urna fermentum.</li>
</ul>
#>
zencode 'div+div>p>span+em^bq'
<# output:
<div></div>
<div>
	<p>
		<span></span>
		<em></em>
	</p>
	<blockquote></blockquote>
</div>
#>
zencode 'html>head>title+body>table>tr>th{name}+th{ID}^(tr>(td>lorem2)*3)*2'
<# output:
<html>
	<head>
		<title></title>
		<body>
			<table>
				<tr>
					<th>name</th>
					<th>ID</th>
				</tr>
				<tr>
					<td>Dolor sit.</td>
					<td>Dolor sit.</td>
					<td>Dolor sit.</td>
				</tr>
				<tr>
					<td>Dolor sit.</td>
					<td>Dolor sit.</td>
					<td>Dolor sit.</td>
				</tr>
			</table>
		</body>
	</head>
</html>
#>

You can have a look at the Zen Coding Cheat Sheet for more examples The syntax might look a bit cryptic at the first glance but once you get the hang of it it’s pretty easy. While this is all already pretty cool I wanted a way to combine this with the PowerShell pipeline in order to do things like that:

zenCode 'ul>li.item{$_}'
<# output
<ul>
	<li class="item">5</li>
	<li class="item">4</li>
	<li class="item">3</li>
	<li class="item">2</li>
	<li class="item">1</li>
</ul>
#>
gps | zenCode 'html>head>title+body>table>tr>th{name}+th{ID}^(tr>td{$_.name}+td{$_.id})'
<# output (excerpt):
<html>
	<head>
		<title></title>
		<body>
			<table>
                                <tr>
					<td>conhost</td>
					<td>14956</td>
				</tr>
				<tr>
					<td>csrss</td>
					<td>600</td>
				</tr>
#>
gps | select -first 10 | zenCode 'html>head>title+body>table[border=1]>(tr>td{$_.Name}+td{$_.ID})+td{$_.Modules | select -first 10}'
<# output (excerpt):
<head>
		<title></title>
		<body>
			<table border="1">
				<tr>
					<th>Name</th>
					<th>ID</th>
					<th>Modules</th>
				</tr>
				<tr>
					<td>iexplore</td>
					<td>8048</td>
					<td></td>
				</tr>
				<tr>
					<td>iexplore</td>
					<td>8488</td>
					<td>
						<table>
							<tr>
								<th>pstypenames</th>
								<th>BaseAddress</th>
								<th>Container</th>
								<th>EntryPointAddress</th>
								<th>FileName</th>
								<th>FileVersionInfo</th>
								<th>ModuleMemorySize</th>
								<th>ModuleName</th>
								<th>Site</th>
								<th>Company</th>
								<th>Description</th>
								<th>FileVersion</th>
								<th>Product</th>
								<th>ProductVersion</th>
								<th>Size</th>
							</tr>
							<tr>
								<td>
									<table>
										<tr>
											<td>System.Diagnostics.ProcessModule</td>
										</tr>
										<tr>
											<td>System.ComponentModel.Component</td>
										</tr>
										<tr>
											<td>System.MarshalByRefObject</td>
										</tr>
										<tr>
											<td>System.Object</td>
										</tr>
									</table>
								</td>
								<td>18612224</td>
								<td></td>
								<td>18619984</td>
								<td>C:\Program Files (x86)\Internet Explorer\IEXPLORE.EXE</td>
								<td>File:             C:\Program Files (x86)\Internet Explorer\IEXPLORE.EXE
InternalName:     iexplore
OriginalFilename: IEXPLORE.EXE.MUI
FileVersion:      11.00.9600.16428 (winblue_gdr.131013-1700)
FileDescription:  Internet Explorer
#>

The solution to make this happen relies on the zencoding implementation Mad Kristensen has developed (https://github.com/madskristensen/zencoding) which he has also part made part of his awesome Web Essentials Visual Studio extension.
I’ve made a PowerShell function (Get-ZenCode alias zenCode) and also an ISE Add-on that expand Zen Code expressions and also work with the PowerShell pipeline. Get-ZenCode also supports output to a file (outPath parameter) and showing the result in the default browser (-Show Switch). I’m posting the source code for Get-ZenCode below but you can also download the same including full help and more examples from my GitHub page (the function requires the zenCoding.dll within a resources subfolder) . The ISE Add-On (including Get-ZenCode) is part of my ISEUtils Add-On. With the Add-On the expressions can be typed into the Editor (without the preceding zenCode call) and expanded by pressing CTRL+SHIFT+J (or the respective menu entry):
ExpandZenCode

How do you like Zen Coding?

shareThoughts


Photo Credit: jurvetson via Compfight cc

Expanding aliases in PowerShell ISE or any PowerShell file

393790664_da5b0ddb12_m
Further extending my PowerShell ISE module (ISEUtils) I’ve added a function to convert aliases either in the currently active ISE file or (in case a a path is provided) within any PowerShell file (that way the function can be also used from the PowerShell Console) to their respective definitions.
Aliases are very useful when working interactively, since they help saving extra keystrokes when you just want to get things done fast. At the same time if we are speaking about production code where readability, and easy comprehension of the code are much more important the usage of aliases should be avoided ( read here for a good article on best practices for PowerShell alias usage).
With the Expand-Alias function you can get the best of both worlds. Writing clearer code while avoiding extraneous keystrokes. For the code samples in my blog posts I’m also using aliases quite a lot, but would like to start using the new function from now on.
Below is the source code for Expand-Alias:

Usage:

  1. If the function is called without any parameter it will expand all aliases within the current ISE file.
  2. Providing a full-path to the path parameter will expand all aliases within the respective PowerShell file instead.

Adding the following lines to your PowerShell profile (you can read here and here on how to work with profiles) will automatically load the function and add an item to the AddOn menu to call it on every ISE startup:

$FULLPATHTOEXPANDALIAS = 'c:\expand-alias.ps1'
. $FULLPATHTOEXPANDALIAS
$null  = $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Expand-Alias', { Expand-Alias }, 'CTRL+SHIFT+X')

shareThoughts


Photo Credit: Martin Gommel via Compfight cc

Create an integrated (WPF based) ISE Add-On with PowerShell

38887680_ffd3c02233_m

The goal of this post is to show you how to create an ISE Add-On that integrates itself graphically in a similar way as the built-in Show-Command Add-On (using the VerticalAddOnTools pane) without having to use Visual Studio and writing the code in C#. As an example I will walk you through the steps to create an Add-On that will generate comment based help for functions. The end product will look like this:
Add-ScriptHelp

While there are quite some tutorials around on how to do this using Visual Studio and C# (e.g.: here) I wanted to be able to do the same using PowerShell only.
I can’t take full credit for the approach since I’ve just modified what James Brundage came up with for his ISEPack Add-On. The function that takes care of the creation of the Add-On is ConvertTo-ISEAddOn. The version I’m using is heavily based on the original version that comes with the ISEPackv2 but also with ShowUI/ConvertTo-ISEAddOn. The main difference between the original and the modified version are additional…:

  • Option to compile the Add-On into a .dll for faster loading (The original version creates the Add-On on the fly)
  • Option to generate multiple Add-Ons and compile them into one .dll
  • In relation to the above I’ve also added options to specify the namespace and classname(s) used for the ISE Add-On(s)
  • Option to add a menu item for the Add-On(s) (this is for testing purpose more on that further down below)

ConverTo-IseAddOn can be used in two different “modes”:

  1. To create one or more ISE Add-On(s) in memory (that’s the way ISEPack is making use of it). :
    ConvertTo-ISEAddOn -ScriptBlock $addScriptHelp -AddVertically -Visible -DisplayName "Add-ScriptHelp" -addMenu
    

    In this case the generated code block is compiled and loaded into memory via Add-Type this would require the code to be re-generated (via ConvertTo-ISEAddOn) on every start of the ISE. I’m using the functionality only for testing purpose while developing a new Add-On.

  2. To create on or more ISE-AddOn(s) and compile them into a .dll.
    ConvertTo-ISEAddOn -ScriptBlock $addScriptHelp -NameSpace $namespace -DLLPath $dllPath -class $classes
    

    This option has the advantage that the code generation needs to happen only once and the usage of the Add-On(s) only requires the .dll to be loaded.

The source code for Convert-ISEAddOn is a bit too long to embed but you can get it from Gist if you want to follow along.
The actual functionality to create the WPF UI, grab the values and generate the comment based help is done in a bit more than 100 lines. In this case I’m using ShowUI to help me generating the WPF UI but this can of course also be done without (you can see some examples of this within some of the other functions that I’ve added to my own ISE Add-On ISEUtils). I’ve updated the function with some AST parsing in order to get the parameter names automatically in case the cursor is placed inside a function while launching the AddOn::

$addScriptHelp ={
    #get the parameters of the enclosing function at the current cursor position if any
    $lineNumber = $psISE.CurrentPowerShellTab.Files.SelectedFile.Editor.CaretLine
    $code = $psISE.CurrentPowerShellTab.Files.SelectedFile.Editor.Text
    $Errors = $Tokens = $null
    $AST = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$Tokens, [ref]$Errors)
    $functions = $AST.FindAll({ $args[0].GetType().Name -like "*FunctionDefinition*Ast" }, $true ) 
    $enclosingFunctionParamNames = -1
    foreach ($function in $functions){
        if ($function.Extent.StartLineNumber -le $lineNumber -and $function.Extent.EndLineNumber -ge $lineNumber){
            if ($function.Body.ParamBlock){
                $enclosingFunctionParamNames = $function.Body.ParamBlock.Parameters.Name.VariablePath.UserPath
            }
            else{
                $enclosingFunctionParamNames = $function.Parameters.Name.VariablePath.UserPath
            }
            break
        }
    }
    $dynamicParams = $false
    if ($enclosingFunctionParamNames -ne -1){
        $dynamicParams = $true
    }
    New-StackPanel {
        New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "Synopsis"
        New-TextBox -Margin "7, 5, 7, 5" -Name "txtSynopsis"
        New-TextBlock -FontSize 17  -Margin "24 2 0 3" -FontWeight Bold -Text "Description"
        New-TextBox -Margin "7, 5, 7, 5" -Name "txtDescription"
        if ($dynamicParams){
            foreach ($paramName in $enclosingFunctionParamNames){
                New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "Parameter description: $paramName" 
                New-TextBox -Margin "7, 5, 7, 5" -Name ("txt$paramName" + 'Desc')
            }
        }
        else{
            New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "1. Param"
            New-TextBox -Margin "7, 5, 7, 5" -Name "txtFirstParamName"
            New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "1. Param Description" 
            New-TextBox -Margin "7, 5, 7, 5" -Name "txtFirstParamDesc"
            New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "2. Param"
            New-TextBox -Margin "7, 5, 7, 5" -Name "txtSecondParamName"
            New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "2. Param Description"
            New-TextBox -Margin "7, 5, 7, 5" -Name "txtSecondParamDesc"
        }
        New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "Link"
        New-TextBox -Margin "7, 5, 7, 5" -Name "txtLink"
        New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "1. Example"
        New-TextBox -Margin "7, 5, 7, 5" -Name "txtFirstExample"
        New-TextBlock -FontSize 17 -Margin "24 2 0 3" -FontWeight Bold -Text "2. Example"
        New-TextBox -Margin "7, 5, 7, 5" -Name "txtSecondExample"
        New-CheckBox -Margin "5, 5, 2, 0"  -Name "chkOutput" {
            New-StackPanel -Margin "3,-5,0,0" {
                New-TextBlock -Name "OutputText" -FontSize 16 -FontWeight Bold -Text "Copy to clipboard"
                New-TextBlock -FontSize 14 TextWrapping Wrap
            }
        }
        New-Button -HorizontalAlignment Stretch -Margin 7 {
            New-TextBlock -FontSize 17 -FontWeight Bold -Text "Add to ISE"
        } -On_Click{
            $txtSynopsis = ($this.Parent.Children | where {$_.Name -eq "txtSynopsis"}).Text 
            $txtDescription = ($this.Parent.Children | where {$_.Name -eq "txtDescription"}).Text
            if ($dynamicParams){
                foreach ($paramName in $enclosingFunctionParamNames){
                    Set-Variable ("txt$paramName" + 'Desc') -Value ($this.Parent.Children | 
                        where {$_.Name -eq ("txt$paramName" + 'Desc')}).Text
                }
            }
            else{
                $txtFirstParamName = ($this.Parent.Children | where {$_.Name -eq "txtFirstParamName"}).Text
                $txtFirstParamDesc = ($this.Parent.Children | where {$_.Name -eq "txtFirstParamDesc"}).Text
                $txtSecondParamName = ($this.Parent.Children | where {$_.Name -eq "txtSecondParamName"}).Text
                $txtSecondParamDesc = ($this.Parent.Children | where {$_.Name -eq "txtSecondParamDesc"}).Text
            }
            $txtLink = ($this.Parent.Children | where {$_.Name -eq "txtLink"}).Text 
            $txtFirstExample = ($this.Parent.Children | where {$_.Name -eq "txtFirstExample"}).Text 
            $txtSecondExample = ($this.Parent.Children | where {$_.Name -eq "txtSecondExample"}).Text 
            $chkOutput = ($this.Parent.Children | where {$_.Name -eq "chkOutput"}).isChecked
            $helptext=@"
    <#    
    .SYNOPSIS
        $txtSynopsis
    .DESCRIPTION
        $txtDescription
"@
            if ($dynamicParams){
                foreach ($paramName in $enclosingFunctionParamNames){
                    $txtParamDesc = Get-Variable -Name ("txt$paramName" + 'Desc') -ValueOnly
                    $helpText+="`n`t.PARAMETER $paramName`n`t`t$txtParamDesc"
                }
            }
            else{
                if ($txtFirstParamName) {
                    $helpText+="`n`t.PARAMETER $txtFirstParamName`n`t`t$txtFirstParamDesc"
                }
                if ($txtSecondParamName) {
                    $helpText+="`n`t.PARAMETER $txtSecondParamName`n`t`t$txtSecondParamDesc"
                }
            }

            if ($txtFirstExample) {
                $helpText+="`n`t.EXAMPLE`n`t`t$txtFirstExample"
            }
            if ($txtSecondExample) {
                $helpText+="`n`t.EXAMPLE`n`t`t$txtSecondExample"
            }
            if ($txtLink) {
                $helpText+="`n`t.LINK`n`t`t$txtLink"
            }
        $helpText+="`n" + @"
    .NOTES 
        CREATED:  $((Get-Date).ToShortDateString())
        AUTHOR      :  $env:USERNAME
	    Changelog:    
	        ----------------------------------------------------------------------------------                                           
	        Name          Date         Description        
	        ----------------------------------------------------------------------------------
	        ----------------------------------------------------------------------------------
  
"@.TrimEnd() + "`n`t#>"
            if ($chkOutput) {
                $helptext | clip
		    } 
            $psise.CurrentPowerShellTab.Files.SelectedFile.Editor.InsertText($helpText)  
        }
    }  
}

For a test run we can use the first mode of ConvertTo-ISEAddOn:

ConvertTo-ISEAddOn -ScriptBlock $addScriptHelp -AddVertically -Visible -DisplayName "Add-ScriptHelp" -addMenu

If you followed along this should successfully generate and load the Add-On (remember that this also requires the ShowUI module to be present). Just in case I’ve also uploaded the complete code so far (+ what follows) separately to GitHub.
Since everything is working fine we can go ahead now and compile the Add-On into a .dll:

$dllPath = "$env:USERPROFILE\Desktop\AddScriptHelp.dll"
ConvertTo-ISEAddOn -ScriptBlock $addScriptHelp -NameSpace ISEUtils -DLLPath $dllPath -class AddScriptHelp

To have the function constantly available in the ISE Add-On we need to add the following to your profile (you can read here and here to see how to work with profiles). This will add an entry to the Add-ons menu and load the Add-On when the entry is clicked:

Add-Type -Path $dllPath
$addScriptHelp = {
    #check if the AddOn if loaded if yes unload and re-load it
    $currentNameIndex = -1
    $name = 'Add-ScriptHelp'
    $currentNames = $psISE.CurrentPowerShellTab.VerticalAddOnTools.Name
    if ($currentNames){
        $currentNameIndex = $currentNames.IndexOf($name)
        if ($currentNameIndex -ne -1){
            $psISE.CurrentPowerShellTab.VerticalAddOnTools.RemoveAt($currentNameIndex)
        }
    }
    $psISE.CurrentPowerShellTab.VerticalAddOnTools.Add($name,[ISEUtils.AddScriptHelp],$true)
    ($psISE.CurrentPowerShellTab.VerticalAddOnTools | where {$_.Name -eq $name}).IsVisible=$true
}
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Add-ScriptHelp', $addScriptHelp, $null)

As mentioned above this function and more is also part of my ISE Add-On ISEUtils

shareThoughts


Photo Credit: Nomad Photography via Compfight cc

Add a default code template to the PowerShell ISE

tree
Some of the 3rd party PowerShell editors offer already built-in support for a default code template where the content of the code template replaces the default blank sheet for every new tab as a starting point for new scripts.
While the PowerShell ISE does not provide this functionality out-of-the-box, it can be quite easily added through the $psISE object model by registering a custom action for the $psise.CurrentPowerShellTab.Files ‘CollectionChanged’ event. This event is triggered whenever a tab is closed or opened:

After pasting and running the above code inside the ISE we first need to create the template. The code template is required to be located at “MyDocuments\WindowsPowerShell\ISETemplate.ps1” it can be created and/or edited using the Edit-ISETemplate function. Once the ISETemplate.ps1 contains some text. Every new tab should now be pre-filled with the content of the code template file.
In order to make this persistent the code should be added to your profile. You can find the path(s) to your profile by running ‘$profile | select *’. I personally favor the ‘CurrentUserAllHosts’ profile since I don’t want to maintain multiple profile files. Host specific code can be added by using conditions like:

if ($host.Name -eq 'Windows PowerShell ISE Host'){
#ISE specific code here
}
elseif ($host.Name -eq 'ConsoleHost'){
#console specific code here
}

I’ve also added this functionality to my ISE Add-On over on GitHub.

shareThoughts


Photo Credit: flavijus via Compfight cc