Skip to content

My first PowerShell ISE Add-In

All hail the mighty PowerShell!


There are a myriad of things you can do with the PowerShell. With it's .NET functionalities and paired with great cmdlets you do not need to develop type-safe scripts. There is virtually nothing to worry about, since all variables, if not cast into a specific type, absorb the type of the object saved to the variable. Cmdlets like Where-Object, Select-Object, Format-Table and the allmighty Get-Member make script developement fun.

PowerShell comes with an integrated scripting environment, the PowerShell ISE, which is supposed to be something like a development environment. This certainly holds true if you just like to debug simple scripts with no advanced stuff like dynamically created script blocks calling dynamically registered functions and so on. Also, there is no possibility to jump between functions, which is the cause for this post.



Writing modules for the PowerShell in PowerShell code is not particularly hard, since the modules behave like normal PowerShell scripts do. Writing a module in C# however does require a tad more programming experience and the innate willingness to sacrifice a substantially large amount of time trying to figure out the PowerShell host model representation in C#.


The first thing I did was seek help in the awesome TechNet forum, where I got the extremely useful advice to do a Get-Member on the one object that controls and reflects the current PowerShell session: $psISE.

TypeName: Microsoft.PowerShell.Host.ISE.ObjectModelRoot

Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
CurrentFile Property Microsoft.PowerShell.Host.ISE.ISEFile CurrentFile {get;}
CurrentPowerShellTab Property Microsoft.PowerShell.Host.ISE.PowerShellTab CurrentPowerShellTab {get;}
CurrentVisibleHorizontalTool Property Microsoft.PowerShell.Host.ISE.ISEAddOnTool CurrentVisibleHorizontalTool {get;}
CurrentVisibleVerticalTool Property Microsoft.PowerShell.Host.ISE.ISEAddOnTool CurrentVisibleVerticalTool {get;}
Options Property Microsoft.PowerShell.Host.ISE.ISEOptions Options {get;}
PowerShellTabs Property Microsoft.PowerShell.Host.ISE.PowerShellTabCollection PowerShellTabs {get;}

As you can see from the output of Get-Member, the .NET object of choice is the ISE Host. Guess what using directive is used... Correct: using Microsoft.PowerShell.Host.ISE;



After finding this out, I set out to create a WPF class library to import in my $profile. After selecting the correct template for an empty class library, the first thing to do is implementing the Interface IAddOnToolHostObject. The only thing needing to be declared is the hostObject, type ObjectModelRoot. I used the shortcut propfull to generate getter and setter for this property. Do whatever you fancy.

ObjectModelRoot hostObject;
public ObjectModelRoot HostObject
{
get
{
return this.hostObject;
}
set
{
this.hostObject = value;
}
}



After this is done, create your xaml. Since I wanted to create a simple ISE add-in I started with a TreeView to accomodate my functions. Since to this date I have not found out how to react to changes to the current file in the ISE properly, i.e. avoiding lock ups and other strange behavior, I also added a simple button labelled "Refresh", triggering a click event.

In order to add child items to the TreeView, I generated a Window_Loaded event that basically uses the PSParser object to tokenize the text in the currently opened ISE tab. At this stage my module iterates over each token, looks for tokens containing the word "function" with the property StartColumn==1. This may seem a bit crude, but in my perfect world functions are declared at the beginning of a line. Every item found is written to the header and the tool tip of the TreeViewItem as well as into a hashtable with line and last column as value.

The same code is used for the click event of the button.

ISEFile currentFile = hostObject.CurrentPowerShellTab.Files.SelectedFile;
int currentCaretLine = currentFile.Editor.CaretLine;
int currentCaretColumn = currentFile.Editor.CaretColumn;
System.Collections.ObjectModel.Collection error = new System.Collections.ObjectModel.Collection();
System.Collections.ObjectModel.Collection currentFileToken = PSParser.Tokenize(currentFile.Editor.Text, out error);
trViewParent.Items.Clear();
functions.Clear();
foreach (PSToken token in currentFileToken)
{

if (token.Content.Contains("function"))
{
TreeViewItem newChild = new TreeViewItem();
if (token.StartColumn == 1)
{
currentFile.Editor.Select(token.StartLine, token.EndColumn + 1, token.StartLine, currentFile.Editor.GetLineLength(token.StartLine) + 1);
newChild.Header = currentFile.Editor.SelectedText;
newChild.ToolTip = currentFile.Editor.SelectedText;
functions.Add(currentFile.Editor.SelectedText, token.StartLine + "," + (currentFile.Editor.GetLineLength(token.StartLine) + 1));
currentFile.Editor.SetCaretPosition(currentCaretLine, currentCaretColumn);
trViewParent.Items.Add(newChild);
}
}
}



In order to jump to functions in your code the module has to react to a SelectedItemChanged event by tokenizing the script content looking for functions and matching them against your hashtable. I just reused the code I already generated for the click event and the Window_Load event and added the following bit for the hashtable lookup:

currentFile.Editor.Select(token.StartLine, token.EndColumn + 1, token.StartLine, currentFile.Editor.GetLineLength(token.StartLine) + 1);
if (currentFile.Editor.SelectedText.Contains(head))
{
Int32 startLine, EndLine;
startLine = Convert.ToInt32(functions[head].ToString().Split(',')[0]);
EndLine = Convert.ToInt32(functions[head].ToString().Split(',')[1]);
currentFile.Editor.SetCaretPosition(startLine, EndLine);
break;
}

As you can see the only interesting thing happening here is the matching of the currently selected TreeViewItem's header property against the selection of text. Afterwards the caret is just set to the column and line stored in the hashtable.



There you have it: A simple module that grabs your script and jumps to your functions. Just import it in your ISE-$profile (%USERPROFILE%\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1) and have a go. A sample import looks like this:

Add-Type -Path 'Path\To\Your\WpfControlLibrary.dll'
$psISE.CurrentPowerShellTab.VerticalAddOnTools.Add(‘Whatever title you would like to see’, [Your complete namespace], $true)

Bear in mind that this is a very simple module! Modify it however you like, for example to include filters and so on. This is by far not finished and will most certainly be updated in the future.



Hopefully you now see that it isn't hard to program PowerShell modules in C#, provided you use the exceedingly useful Get-Member cmdlet. If you are interested, you can download the whole C# project here


Copyright (c) 2013 Jan-Hendrik Peters

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Trackbacks

Keine Trackbacks

Kommentare

Ansicht der Kommentare: Linear | Verschachtelt

Noch keine Kommentare

Die Kommentarfunktion wurde vom Besitzer dieses Blogs in diesem Eintrag deaktiviert.

Kommentar schreiben

Umschließende Sterne heben ein Wort hervor (*wort*), per _wort_ kann ein Wort unterstrichen werden.
Standard-Text Smilies wie :-) und ;-) werden zu Bildern konvertiert.
Formular-Optionen

Kommentare werden erst nach redaktioneller Prüfung freigeschaltet!