Easily manage files in Powershell ISE CTP3

Since Win7 debuted, I’ve started forcing myself to use the ISE more often.  It doesn’t have a ton of features – even the much awaited debugger lags some v1.0-era 3rd party IDEs – but it does show great potential.  In particular, who doesn’t love an environment that’s scriptable using its native language? :) 

[and oh yeah, we finally get to leave the world of CSRSS.exe behind.  Ctrl+V!  sweet blessed Ctrl+V!]

Naturally, I started the customization process by scouring the web.  Keith’s "Yank Line” function quickly joined my Custom menu.  (though I map it to Ctrl+K, a vestige of muscle memory from Pico.  thanks for the memories, crappy Solaris boxes at Duke!)  Soon after came a function templater from Andy Schneider, handy especially since the new v2 syntax is pretty verbose.  And of course Lee’s syntax highlighter, without which the pretty code you’re seeing would not be possible – though in full disclosure I’ve heavily modified it.  (first to fit into a Module, then to remove the Apartment Threading overhead that’s thankfully not relevant in the ISE, and finally to take the currently selected text instead of the whole document)

Any other recommendations for $profile inclusion?  Or know how to override a keyboard shortcut, for that matter?

Back to the point of this post.  My work style tends to involve dozens of tabs, files scattered all over the place, saved & not, until I clean things up hours later.  So naturally the built-in file management features of ISE have got to go!  With the help of the $psise object model, the get-fullpath script I introduced last time, and liberal additions to the ISE profile (Microsoft.PowerShellISE_profile.ps1), it’s not hard to greatly beef things up:

  • quick Open from the interactive prompt, including rich pipeline support seen in previous posts
  • ditto for Close
  • Close All But This
  • Undo Close with a full FIFO stack of everything you’ve ever closed using my functions
  • Save All
  • Copy Full Path to clipboard
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# quick open from the command prompt. Pipeline enabled :)
function ise 
{
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$True, Position=0, ValueFromPipeline=$True)]
        [psobject]$file,
        [Parameter()]
        [switch] $PassThru = $false 
    )   
    process 
    {
        if ($PassThru)
            { $file }       
        $psise.CurrentOpenedRunspace.OpenedFiles.Add(($file | get-fullpath))
    }
}

# ditto for close, with my desired keyboard shortcuts
function close
{
    [CmdletBinding()]
    param (
        [parameter(Position=0, ValueFromPipeline=$True)
        [psobject]$file = ""
    )
   
    begin
    {
        # getting desired behavior of "no input = close current file" is harder than it looks...
        $closedSomething = $false
       
        # set up global undo buffer
        if ($RecentlyClosedTabs -eq $null)
            { set-variable -name RecentlyClosedTabs -value (new-object system.collections.stack) -scope Global }
    }
   
    process
    {
        if ($file -ne "")  # thought I could use the 'continue' statement instead of a giant If block, but it exits the whole function
        {       
            $fullPath = $file | get-fullpath
            if (@($fullPath).count -gt 0)  # can't simply check against null for some weird reason
            {
                # because there is no constructor for System.Management.Automation.Host.OpenedFile, I have to do stupid iteration
                # that makes this routine n^2
                $fileToClose = $psise.CurrentOpenedRunspace.OpenedFiles | 
                    ? { $_.fullpath -eq $fullpath }                   
                $psise.CurrentOpenedRunspace.OpenedFiles.Remove($fileToClose) | out-null
                $closedSomething = $true
                $RecentlyClosedTabs.Push($fullPath)
            }
        }
    }
   
    end
    {
        if (! $closedSomething)
        {
            $curFile = $psise.CurrentOpenedFile
            $psise.CurrentOpenedRunspace.OpenedFiles.Remove($curFile) | out-null
            $RecentlyClosedTabs.Push($curFile) 
        }
       
        # after we've closed a file, chances are we don't want the cursor stuck in another random file
        $psise.CurrentOpenedRunspace.CommandPane.Focus()
    }
}
# $psISE.CustomMenu.Submenus.Add('_Close File', {close}, 'Ctrl+W') # can't overwrite keyboard shortcuts :(

function CloseAllButThis
{
    # need to clone the array of opened files since we'll be modifying it as we enumerate
    $temp = [array]::createinstance([System.Management.Automation.Host.OpenedFile], $psise.CurrentOpenedRunspace.OpenedFiles.Count)
    $psise.CurrentOpenedRunspace.OpenedFiles.CopyTo($temp, 0)
   
    # edge case: if there's only 1 file open, do nothing
    if ($temp.count -eq 1)
        { break }
 
    # close the files
    $temp | ? { $_ -ne $psise.CurrentOpenedFile } | close
}
$psISE.CustomMenu.Submenus.Add('Close All But _This', {CloseAllButThis}, 'Ctrl+Shift+W')

# this will spew errors when there are Untitled files, but I kinda like that behavior
function SaveAll
{   
    $psise.CurrentOpenedRunspace.OpenedFiles | % { $_.Save() }
}
$psISE.CustomMenu.Submenus.Add('_Save All', {CloseAllButThis}, 'Ctrl+Alt+S')  # really want Ctrl+Shift+S :(

# undo close tab(s)
function UndoClose ([int] $files = 1)
{
    if ($RecentlyClosedTabs -eq $null -or $RecentlyClosedTabs.Count -le 0 -or $files -le 0)
        { break }
   
    # if user tries to pop more items than we have on the stack, just let the error bubble up
    while($files -gt 0)
    {
        $RecentlyClosedTabs.Pop() | ise
        --$files
    }
}
$psISE.CustomMenu.Submenus.Add('_Undo Close', {UndoClose}, 'Ctrl+Alt+U')

# copy full path of the open file to the clipboard
function Get-OpenedPath
{
    $psise.CurrentOpenedFile.FullPath | out-clipboard
}
$psISE.CustomMenu.Submenus.Add('Copy Full _Path', {Get-OpenedPath}, 'Ctrl+Alt+C')

As you can see, the Close function ended up much uglier than expected.  Powershell limitations/bugs or programmer ignorance?  Suggestions welcome.  Powershell verb-noun Nazis need not apply.  Functions which live only in the $profile for quick consumption at the interactive prompt and/or ISE shortcut key are not obligated to follow the rules for discoverability, IMO.

Leave a Reply