Archive

Posts Tagged ‘keyboard shortcuts’

Launch Visual Studio Checkin Window With A Keystroke

September 27th, 2013 No comments

A few weeks ago I blogged about how you can get custom TFS checkin policies to work when committing from the command line. In that post, I had a quick aside about how you can launch the checkin window (i.e. pending changes) with a quick keystroke using AutoHotkey.  I realize that many people don’t use AutoHotkey (although you really should; it can save you a lot of time), so I thought I would show how you can accomplish this task without AutoHotkey.  It’s quite simple really, and it allows you to launch the VS Checkin window from anywhere, even if you don’t have Visual Studio open.

 

Steps To Launch VS Checkin Window From The Visual Studio Command Prompt

  1. Open the Visual Studio Command Prompt.  To do this, just hit the windows key and type Developer Command Prompt For VS2012 if using VS 2012, or Visual Studio Command Prompt (2010) if using VS 2010.
  2. In the VS Command Prompt, change to a directory that is in your TFS workspace mapping. e.g. cd C:\Dev\TFS
  3. Type tf checkin and hit enter.

    Steps To Launch VS Checkin Window With A Shortcut Key

    1. Right click on your desktop and choose New –> Shortcut to create a new shortcut file.
      CreateShortcutOnDesktop
    2. Have the shortcut point to the TF executable. This can be found at "C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\TF.exe".
      PathToTfExeForShortcut

    3. Enter the name for your shortcut file, such as VS Checkin.
      NameShortcut

    4. Now that the shortcut has been created on your desktop, right-click on it and go the the properties (or use alt+enter). You will want to:
    • add checkin to the very end of the Target,
    • change the Start In directory to a directory you have mapped in your TFS workspace,
    • and assign it a Shortcut Key. 
      CheckinShortcutPropertiesToChange

     

    Results

    That’s it.  Go ahead and try your shortcut key.  I’ll wait.  You should see something like this:

    VsCheckinWindow

    Notice that it pops both a command prompt window and the actual checkin window.  If you don’t have any pending changes, then the command prompt window will simply open and close.

     

    More Information and Caveats

    Old Style Checkin Window

    You probably noticed in the screenshot above that even though I’m using Visual Studio 2012, it still pops the old VS 2010 style of checkin window.  I actually prefer this popped out window to the VS 2012 pending changes pane, and I know a lot of people agree.

     

    Getting The Shortcut Off Your Desktop

    Above I had you create the shortcut on your desktop, but you might not want to have it clutter up your desktop. Unfortunately if you move it to some other folder you will find that the shortcut key no longer works.  For some reason I have found that for the shortcut key to work the shortcut file must either be on your desktop, or in the Start Menu. If you are using Windows 7 you can simply drag and drop the shortcut into the Programs section of the Start Menu.  For us Windows 8.0 folks the Start Menu is gone, so just manually move the shortcut to “C:\ProgramData\Microsoft\Windows\Start Menu\Programs”.

    You may find that after moving the shortcut file the shortcut key no longer works. You just have to go back into the Shortcut file properties, assign it a new Shortcut Key, hit Apply, and then assign the original Shortcut Key back.

     

    Shelving Changes In TFS Using The Old Style Shelve Window

    If you are using TFS and want to shelve your changes instead of checking them in, you can access the old Shelve Pending Changes window in the same way.  In step 4 above, instead of adding checkin as the TF.exe argument, add shelve.  To launch it from the Visual Studio Command Prompt, type tf shelve instead of tf checkin.

     

    Getting Custom TFS Checkin Policies To Work

    As you may have guessed from the very start of the article, custom TFS checkin policies don’t run in this checkin window by default; they will throw errors.  Fortunately for you I have already created a registry file that you can run after each checkin policy update that you do which will rectify this problem.

     

    I hope this helps you be more productive.  Happy coding!

    PowerShell ISE: Multi-line Comment and Uncomment Done Right, and other ISE GUI must haves

    June 19th, 2013 16 comments

    I’ve written some code that you can add to your ISE profile that adds keyboard shortcuts to quickly comment and uncomment lines in PowerShell ISE.  So you can quickly turn this:

    This is some
    	code and here is
    some more code.
    

    into this:

    #This is some
    #	code and here is
    #some more code.
    

    and back again.

    Feel free to skip the Preamble and get right to the good stuff.

     

    Preamble

    I’ve only been writing PowerShell (PS) for about 6 months now, and have a love-hate relationship with it.  It is simply a wonderful tool…once you understand how it works and have learnt some of the nuances.  I’ve gotten hung up for hours on end with things that should be simple, but aren’t.  For example, if you have an array of strings, but the array actually only contains a single string, when you go to iterate over the array instead of giving you the string it will iterator over the characters in the string….but if you have multiple strings in your array then everything works fine (btw the trick is you have to explicitly cast your array to a string array when iterating over it).  This is only one small example, but I’ve found I’ve hit many little Gotcha’s like this since I started with PS.  So PS is a great tool, but has a deceptively steep learning curve in my opinion; it’s easy to get started with it, especially if you have a .Net background, but there are many small roadblocks that just shouldn’t be there.  Luckily, we have Stack Overflow Smile

    Anyways, as a PS newb one of the first things I did was go look for a nice editor to work in; intellisense was a must.  First I tried PowerShell ISE v3 since it comes with Windows out of the box, but was quickly turned off at how featureless the GUI is.  Here’s a quick list of lacking UI components that immediately turned me off of ISE’s Script Pane:

    1. No keyboard shortcut to quickly comment/uncomment code (go up-vote to get this added).
    2. No “Save All Files” keyboard shortcut (go up-vote to get this added).
    3. No ability to automatically reopen files that were open when I closed ISE; there’s the Recent Documents menu, but that’s an extra 10 clicks every time I open ISE (go up-vote to get this added).
    4. Can not split the tab windows to show two files side by side (go up-vote to get this added).
    5. Can not drag a tab out of ISE to show it on another monitor (go up-vote to get this added).
    6. Can not enter tabs on the end of lines; I do this all of the time to line up my comments placed on the end of the code line. I’m guessing this is “by design” though to allow the tab-completion to work (I show a workaround for this in this post).
    7. Find/Replace window does not have an option to wrap around the end of the file; it will only search down or up depending on if the Search Up checkbox is checked (go up-vote to get this added).
    8. Can’t simply use Ctrl+F3 to search for the current/selected word/text; you have to use the actual Find window (go up-vote to get this added).
    9. When you perform an undo/redo, the caret and view don’t jump to the text being undone/redone, so if the text being changed is outside of the viewable area you can’t see what is being changed (up-vote to get this fixed).
    10.   Can not re-arrange tabs; you have to close and reopen them if you want to change their order (go up-vote to get this added).
    11.   The intellisense sometimes becomes intermittent or stops all together and you have to restart ISE (go up-vote to get this fixed).
    12.   Double-clicking a cmdlet or variable name does not select the entire cmdlet/variable name; e.g. doesn’t fully select “Get-Help” or “$variable” (go up-vote to get this added).

    It took me all of 5 minutes to say “ISE is not a mature enough editor for me”; I guess I’ve been spoiled by working in Visual Studio for so many years.  So I went and found PowerGUI, which was pretty good and I liked it quite a bit at first.  It’s been a while since I’ve used it so honestly I can’t remember all of the reasons why I decided to switch away from it.  I remember one problem of having to constantly start a new PS session in order to pick up changes to functions that I made (I think they had a button for that at least), as well as intellisense not being reliable, and having problems with debugging.  Anyways, I decided to switch to PowerShellPlus and was much happier with it.  It still wasn’t perfect; I still had problems with intellisense and debugging, but I was still happy.  I especially liked that I could search for and download other people’s script easily from it, which is great for learning.  As I kept using it though, it kept taking longer and longer to load.  After about 3 months I found myself waiting about a minute for it to open, and then once it was open, another 15 seconds or so to open all of my previously open tabs; and I have an SSD.  So I thought I would give ISE another shot, mainly because it is already installed by default and I now know that I can customize it somewhat with the add-ons.

     

    Other Must Have ISE GUI Add-ons

    After looking for not too long, I found posts on the PowerShell Team’s blog which address the Save All and Save/Restore ISE State issues (#2 and #3 in my list above).  These are must haves, and I provide them alongside my code in the last section below.

     

    Why My Implementation Is Better

    Other solutions and why they suck:

    So of course before writing my own multiline comment/uncomment code I went searching for an existing solution, and I did find two.  The first one was recommended by Ed Wilson (aka Hey, Scripting Guy!) at the bottom of this post.  He recommended using the PowerShellPack.  I downloaded it, added it to my PS profile, and gave it a try.  I was instantly disappointed.  The other solution I found was by Clatonh (a Microsoft employee).  Again, I added his code to my ISE profile to try it out, and was disappointed.

    Here are the problems with their solutions:

    1. If you only have part of a line selected, it places the comment character at the beginning of your selection, not at the beginning of the line (undesirable, both).
    2. If you don’t have any text selected, nothing gets commented out (undesirable, both).
    3. If you have any blank lines selected in your multiline selection, it removes them (unacceptable, PowerShellPack only).
    4. It uses block comments (i.e. <# … #>)! (unacceptable (block comments are the devil), Clatonh’s solution only) I’m not sure if the PowerShellPack problems are because it was written for PS v2 and I’m using v3 on Windows 8, but either way that was unacceptable for me.
      You might be wondering why #4 is on my list and why I hate block comments so much.  Block comments themselves aren’t entirely a bad idea; the problem is that 99% of editors (including PS ISE) don’t handle nested block comments properly.  For example, if I comment out 3 lines in a function using block comments, and then later go and comment out the entire function using block comments, I’ll get a compiler error (or in PS’s case, a run-time error); this is because the first closing “#>” tag will be considered the closing tag for both the 1st and 2nd opening “<#” tags; so everything between the 1st and 2nd closing “#>” tag won’t actually be commented out.  Because of this it is just easier to avoid block comments all together, even for that paragraph of comment text you are about to write (you do comment your code, right?).

    My Solution:

    1. Uses single line comments (no block comments!).
    2. Places the comment character at the beginning of the line, even if you have middle of line selected.
    3. Comments out the line that the caret is on if no text is selected.
    4. Preserves blank lines, and doesn’t comment them out.

      Show Me The Code

      Before I give you the code, we are going to want to add it to your PowerShell ISE profile, so we need to open that file.

      To edit your PowerShell ISE profile:

      1. Open Windows PowerShell ISE (not Windows PowerShell, as we want to edit the ISE profile instead of the regular PowerShell profile).
      2. In the Command window type: psedit $profile

        If you get an error that it cannot find the path, then first type the following to create the file before trying #2 again: New-Item $profile –ItemType File –Force

      And now that you have your PowerShell ISE profile file open for editing, here’s the code to append to it in order to get the comment/uncomment commands and keyboard shortcuts (or keep reading and get ALL the code from further down).  You will then need to restart PowerShell ISE for the new commands to show up and work.  I’ll mention too that I’ve only tested this on Windows 8 with PowerShell v3.0.

      # Define our constant variables.
      [string]$NEW_LINE_STRING = "`r`n"
      [string]$COMMENT_STRING = "#"
      
      function Select-EntireLinesInIseSelectedTextAndReturnFirstAndLastSelectedLineNumbers([bool]$DoNothingWhenNotCertainOfWhichLinesToSelect = $false)
      {
      <#
          .SYNOPSIS
          Exands the selected text to make sure the entire lines are selected.
          Returns $null if we can't determine with certainty which lines to select and the 
      
          .DESCRIPTION
          Exands the selected text to make sure the entire lines are selected.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToSelect
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to select the entire selected lines, but we may guess wrong and select the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be selected.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may comment out the 1st and 2nd lines correctly, or it may comment out the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get selected, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      
          .OUTPUT
          PSObject. Returns a PSObject with the properties FirstLineNumber and LastLineNumber, which correspond to the first and last line numbers of the selected text.
      #>
      
          # Backup all of the original info before we modify it.
          [int]$originalCaretLine = $psISE.CurrentFile.Editor.CaretLine
          [string]$originalSelectedText = $psISE.CurrentFile.Editor.SelectedText
          [string]$originalCaretLineText = $psISE.CurrentFile.Editor.CaretLineText
      
          # Assume only one line is selected.
          [int]$textToSelectFirstLine = $originalCaretLine
          [int]$textToSelectLastLine = $originalCaretLine
      
          #------------------------
          # Before we process the selected text, we need to make sure all selected lines are fully selected (i.e. the entire line is selected).
          #------------------------
      
          # If no text is selected, OR only part of one line is selected (and it doesn't include the start of the line), select the entire line that the caret is currently on.
          if (($psISE.CurrentFile.Editor.SelectedText.Length -le 0) -or !$psISE.CurrentFile.Editor.SelectedText.Contains($NEW_LINE_STRING))
          {
              $psISE.CurrentFile.Editor.SelectCaretLine()
          }
          # Else the first part of one line (or the entire line), or multiple lines are selected.
          else
          {
              # Get the number of lines in the originally selected text.
              [string[]] $originalSelectedTextArray = $originalSelectedText.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
              [int]$numberOfLinesInSelectedText = $originalSelectedTextArray.Length
      
              # If only one line is selected, make sure it is fully selected.
              if ($numberOfLinesInSelectedText -le 1)
              {
                  $psISE.CurrentFile.Editor.SelectCaretLine()
              }
              # Else there are multiple lines selected, so make sure the first character of the top line is selected (so that we put the comment character at the start of the top line, not in the middle).
              # The first character of the bottom line will always be selected when multiple lines are selected, so we don't have to worry about making sure it is selected; only the top line.
              else
              {
                  # Determine if the caret is on the first or last line of the selected text.
                  [bool]$isCaretOnFirstLineOfSelectedText = $false
                  [string]$firstLineOfOriginalSelectedText = $originalSelectedTextArray[0]
                  [string]$lastLineOfOriginalSelectedText = $originalSelectedTextArray[$originalSelectedTextArray.Length - 1]
      
                  # If the caret is definitely on the first line.
                  if ($originalCaretLineText.EndsWith($firstLineOfOriginalSelectedText) -and !$originalCaretLineText.StartsWith($lastLineOfOriginalSelectedText))
                  {
                      $isCaretOnFirstLineOfSelectedText = $true
                  }
                  # Else if the caret is definitely on the last line.
                  elseif ($originalCaretLineText.StartsWith($lastLineOfOriginalSelectedText) -and !$originalCaretLineText.EndsWith($firstLineOfOriginalSelectedText))
                  {
                      $isCaretOnFirstLineOfSelectedText = $false
                  }
                  # Else we need to do further analysis to determine if the caret is on the first or last line of the selected text.
                  else
                  {
                      [int]$numberOfLinesInFile = $psISE.CurrentFile.Editor.LineCount
      
                      [string]$caretOnFirstLineText = [string]::Empty
                      [int]$caretOnFirstLineArrayStartIndex = ($originalCaretLine - 1) # -1 because array starts at 0 and file lines start at 1.
                      [int]$caretOnFirstLineArrayStopIndex = $caretOnFirstLineArrayStartIndex + ($numberOfLinesInSelectedText - 1) # -1 because the starting line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                      [string]$caretOnLastLineText = [string]::Empty
                      [int]$caretOnLastLineArrayStopIndex = ($originalCaretLine - 1)  # -1 because array starts at 0 and file lines start at 1.
                      [int]$caretOnLastLineArrayStartIndex = $caretOnLastLineArrayStopIndex - ($numberOfLinesInSelectedText - 1) # -1 because the stopping line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                      # If the caret being on the first line would cause us to go "off the file", then we know the caret is on the last line.
                      if (($caretOnFirstLineArrayStartIndex -lt 0) -or ($caretOnFirstLineArrayStopIndex -ge $numberOfLinesInFile))
                      {
                          $isCaretOnFirstLineOfSelectedText = $false
                      }
                      # If the caret being on the last line would cause us to go "off the file", then we know the caret is on the first line.
                      elseif (($caretOnLastLineArrayStartIndex -lt 0) -or ($caretOnLastLineArrayStopIndex -ge $numberOfLinesInFile))
                      {
                          $isCaretOnFirstLineOfSelectedText = $true
                      }
                      # Else we still don't know where the caret is.
                      else
                      {
                          [string[]]$filesTextArray = $psISE.CurrentFile.Editor.Text.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
      
                          # Get the text of the lines where the caret is on the first line of the selected text.
                          [string[]]$caretOnFirstLineTextArray = @([string]::Empty) * $numberOfLinesInSelectedText # Declare an array with the number of elements required.
                          [System.Array]::Copy($filesTextArray, $caretOnFirstLineArrayStartIndex, $caretOnFirstLineTextArray, 0, $numberOfLinesInSelectedText)
                          $caretOnFirstLineText = $caretOnFirstLineTextArray -join $NEW_LINE_STRING
      
                          # Get the text of the lines where the caret is on the last line of the selected text.
                          [string[]]$caretOnLastLineTextArray = @([string]::Empty) * $numberOfLinesInSelectedText # Declare an array with the number of elements required.
                          [System.Array]::Copy($filesTextArray, $caretOnLastLineArrayStartIndex, $caretOnLastLineTextArray, 0, $numberOfLinesInSelectedText)
                          $caretOnLastLineText = $caretOnLastLineTextArray -join $NEW_LINE_STRING
      
                          [bool]$caretOnFirstLineTextContainsOriginalSelectedText = $caretOnFirstLineText.Contains($originalSelectedText)
                          [bool]$caretOnLastLineTextContainsOriginalSelectedText = $caretOnLastLineText.Contains($originalSelectedText)
      
                          # If the selected text is only within the text of when the caret is on the first line, then we know for sure the caret is on the first line.
                          if ($caretOnFirstLineTextContainsOriginalSelectedText -and !$caretOnLastLineTextContainsOriginalSelectedText)
                          {
                              $isCaretOnFirstLineOfSelectedText = $true
                          }
                          # Else if the selected text is only within the text of when the caret is on the last line, then we know for sure the caret is on the last line.
                          elseif ($caretOnLastLineTextContainsOriginalSelectedText -and !$caretOnFirstLineTextContainsOriginalSelectedText)
                          {
                              $isCaretOnFirstLineOfSelectedText = $false
                          }
                          # Else if the selected text is in both sets of text, then we don't know for sure if the caret is on the first or last line.
                          elseif ($caretOnFirstLineTextContainsOriginalSelectedText -and $caretOnLastLineTextContainsOriginalSelectedText)
                          {
                              # If we shouldn't do anything since we might comment out text that is not selected by the user, just exit this function and return null.
                              if ($DoNothingWhenNotCertainOfWhichLinesToSelect)
                              {
                                  return $null
                              }
                          }
                          # Else something went wrong and there is a flaw in this logic, since the selected text should be in one of our two strings, so let's just guess!
                          else
                          {
                              Write-Error "WHAT HAPPENED?!?! This line should never be reached. There is a flaw in our logic!"
                              return $null
                          }
                      }
                  }
      
                  # Assume the caret is on the first line of the selected text, so we want to select text from the caret's line downward.
                  $textToSelectFirstLine = $originalCaretLine
                  $textToSelectLastLine = $originalCaretLine + ($numberOfLinesInSelectedText - 1) # -1 because the starting line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                  # If the caret is actually on the last line of the selected text, we want to select text from the caret's line upward.
                  if (!$isCaretOnFirstLineOfSelectedText)
                  {
                      $textToSelectFirstLine = $originalCaretLine - ($numberOfLinesInSelectedText - 1) # -1 because the stopping line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
                      $textToSelectLastLine = $originalCaretLine
                  }
      
                  # Re-select the text, making sure the entire first and last lines are selected. +1 on EndLineWidth because column starts at 1, not 0.
                  $psISE.CurrentFile.Editor.Select($textToSelectFirstLine, 1, $textToSelectLastLine, $psISE.CurrentFile.Editor.GetLineLength($textToSelectLastLine) + 1)
              }
          }
      
          # Return the first and last line numbers selected.
          $selectedTextFirstAndLastLineNumbers = New-Object PSObject -Property @{
              FirstLineNumber = $textToSelectFirstLine
              LastLineNumber = $textToSelectLastLine
          }
          return $selectedTextFirstAndLastLineNumbers
      }
      
      function CommentOrUncommentIseSelectedLines([bool]$CommentLines = $false, [bool]$DoNothingWhenNotCertainOfWhichLinesToSelect = $false)
      {
          $selectedTextFirstAndLastLineNumbers = Select-EntireLinesInIseSelectedTextAndReturnFirstAndLastSelectedLineNumbers $DoNothingWhenNotCertainOfWhichLinesToSelect
      
          # If we couldn't determine which lines to select, just exit without changing anything.
          if ($selectedTextFirstAndLastLineNumbers -eq $null) { return }
      
          # Get the text lines selected.
          [int]$selectedTextFirstLineNumber = $selectedTextFirstAndLastLineNumbers.FirstLineNumber
          [int]$selectedTextLastLineNumber = $selectedTextFirstAndLastLineNumbers.LastLineNumber
      
          # Get the Selected Text and convert it into an array of strings so we can easily process each line.
          [string]$selectedText = $psISE.CurrentFile.Editor.SelectedText
          [string[]] $selectedTextArray = $selectedText.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
      
          # Process each line of the Selected Text, and save the modified lines into a text array.
          [string[]]$newSelectedTextArray = @()
          $selectedTextArray | foreach {
              # If the line is not blank, add a comment character to the start of it.
              [string]$lineText = $_
              if ([string]::IsNullOrWhiteSpace($lineText)) { $newSelectedTextArray += $lineText }
              else 
              {
                  # If we should be commenting the lines out, add a comment character to the start of the line.
                  if ($CommentLines) 
                  { $newSelectedTextArray += "$COMMENT_STRING$lineText" }
                  # Else we should be uncommenting, so remove a comment character from the start of the line if it exists.
                  else 
                  {
                      # If the line begins with a comment, remove one (and only one) comment character.
                      if ($lineText.StartsWith($COMMENT_STRING))
                      {
                          $lineText = $lineText.Substring($COMMENT_STRING.Length)
                      } 
                      $newSelectedTextArray += $lineText
                  }
              }
          }
      
          # Join the text array back together to get the new Selected Text string.
          [string]$newSelectedText = $newSelectedTextArray -join $NEW_LINE_STRING
      
          # Overwrite the currently Selected Text with the new Selected Text.
          $psISE.CurrentFile.Editor.InsertText($newSelectedText)
      
          # Fully select all of the lines that were modified. +1 on End Line's Width because column starts at 1, not 0.
          $psISE.CurrentFile.Editor.Select($selectedTextFirstLineNumber, 1, $selectedTextLastLineNumber, $psISE.CurrentFile.Editor.GetLineLength($selectedTextLastLineNumber) + 1)
      }
      
      function Comment-IseSelectedLines([switch]$DoNothingWhenNotCertainOfWhichLinesToComment)
      {
      <#
          .SYNOPSIS
          Places a comment character at the start of each line of the selected text in the current PS ISE file.
          If no text is selected, it will comment out the line that the caret is on.
      
          .DESCRIPTION
          Places a comment character at the start of each line of the selected text in the current PS ISE file.
          If no text is selected, it will comment out the line that the caret is on.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToComment
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to comment out the selected lines, but we may guess wrong and comment out the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be commented out.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may comment out the 1st and 2nd lines correctly, or it may comment out the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get commented out, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      #>
          CommentOrUncommentIseSelectedLines -CommentLines $true -DoNothingWhenNotCertainOfWhichLinesToSelect $DoNothingWhenNotCertainOfWhichLinesToComment
      }
      
      function Uncomment-IseSelectedLines([switch]$DoNothingWhenNotCertainOfWhichLinesToUncomment)
      {
      <#
          .SYNOPSIS
          Removes the comment character from the start of each line of the selected text in the current PS ISE file (if it is commented out).
          If no text is selected, it will uncomment the line that the caret is on.
      
          .DESCRIPTION
          Removes the comment character from the start of each line of the selected text in the current PS ISE file (if it is commented out).
          If no text is selected, it will uncomment the line that the caret is on.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToUncomment
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to uncomment the selected lines, but we may guess wrong and uncomment out the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be uncommentet.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may uncomment the 1st and 2nd lines correctly, or it may uncomment the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get uncommented, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      #>
          CommentOrUncommentIseSelectedLines -CommentLines $false -DoNothingWhenNotCertainOfWhichLinesToSelect $DoNothingWhenNotCertainOfWhichLinesToUncomment
      }
      
      
      #==========================================================
      # Add ISE Add-ons.
      #==========================================================
      
      # Add a new option in the Add-ons menu to comment all selected lines.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Comment Selected Lines" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Comment Selected Lines",{Comment-IseSelectedLines},"Ctrl+K")
      }
      
      # Add a new option in the Add-ons menu to uncomment all selected lines.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Uncomment Selected Lines" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Uncomment Selected Lines",{Uncomment-IseSelectedLines},"Ctrl+Shift+K")
      }
      

      As you can see by the code at the bottom, the keyboard shortcut to comment lines is Ctrl+K and to uncomment it is Ctrl+Shift+K.  Feel free to change these if you like.  I wanted to use the Visual Studio keyboard shortcut keys of Ctrl+K,Ctrl+C and Ctrl+K,Ctrl+U, but it looks like multi-sequence keyboard shortcuts aren’t supported.  I figured that anybody who uses Visual Studio or SQL Server Management Studio would be able to stumble across this keyboard shortcut and would like it.

       

      Ok, it’s not perfect:

      If you’re still reading then you deserve to know about the edge case bug with my implementation.  If you actually read through the functions’ documentation in the code you will see this mentioned there as well.

      Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may comment out the 1st and 2nd lines correctly, or it may comment out the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get uncommented, so it shouldn't be a big deal.
      

      Basically the problem is that I change the selected text to ensure that the entire lines are selected (so that I can put the comment character at the start of the line).  The PS ISE API doesn’t tell me the selected text’s starting and ending lines, so I have to try and infer it from the line the caret is on, but the caret can be on either the first or the last line of the selected text.  So if text that is identical to the selected text appears directly above or below the selected text, I can’t know for sure if the caret is on the first line of the selected text, or the last line, so I just make a guess.  If this bothers you there is a switch you can provide so that it won’t comment out any lines at all if this edge case is hit.

       

      Show Me ALL The Code

      Ok, so I mentioned a couple other must-have ISE add-ons above.  Here’s the code to add to your ISE profile that includes my comment/uncomment code, as well as the Save All files and Save/Restore ISE State functionality provided by the PowerShell Team.  This includes a couple customizations that I made; namely adding a Save ISE State And Exit command (Alt+Shift+E) and having the ISE State automatically load when PS ISE starts (I didn’t change the functions they provided that do the actual work at all). So if you want your last session to be automatically reloaded, you just have to get in the habit of closing ISE with Alt+Shift+E (again, you can change this keyboard shortcut if you want).

      #==========================================================
      # Functions used by the script.
      #==========================================================
      
      function Save-AllISEFiles
      {
      <#
      .SYNOPSIS
          Saves all ISE Files except for untitled files. If You have multiple PowerShellTabs, saves files in all tabs.
      #>
          foreach($tab in $psISE.PowerShellTabs)
          {
              foreach($file in $tab.Files)
              {
                  if(!$file.IsUntitled)
                  {
                      $file.Save()
                  }
              }
          }
      }
      
      function Export-ISEState
      {
      <#
      .SYNOPSIS
          Stores the opened files in a serialized xml so that later the same set can be opened
       
      .DESCRIPTION
          Creates an xml file with all PowerShell tabs and file information
         
      .PARAMETER fileName
          The name of the project to create a new version from. This will also be the name of the new project, but with a different version
       
      .EXAMPLE
          Stores current state into c:\temp\files.isexml
          Export-ISEState c:\temp\files.isexml
      #>
       
          Param
          (
              [Parameter(Position=0, Mandatory=$true)]
              [ValidateNotNullOrEmpty()]
              [string]$fileName
          )
         
          # We are exporting a "tree" worth of information like this:
          #
          #  SelectedTabDisplayName: PowerShellTab 1
          #  SelectedFilePath: c:\temp\a.ps1
          #  TabInformation:
          #      PowerShellTab 1:
          #           File 1:
          #                FullPath:     c:\temp\a.ps1
          #                FileContents: $null
          #           File 2:
          #                FullPath:     Untitled.ps1
          #                FileContents: $a=0...
          #       PowerShellTab 2:
          #       ...
          #  Hashtables and arraylists serialize rather well with export-clixml
          #  We will keep the list of PowerShellTabs in one ArrayList and the list of files
          #  and contents(for untitled files) inside each tab in a couple of ArrayList.
          #  We will use Hashtables to group the information.
          $tabs=new-object collections.arraylist
         
          # before getting file information, save all untitled files to make sure their latest
          # text is on disk
          Save-AllISEFiles
       
          foreach ($tab in $psISE.PowerShellTabs)
          {
              $files=new-object collections.arraylist
              $filesContents=new-object collections.arraylist
              foreach($file in $tab.Files)
              {
                  # $null = will avoid $files.Add from showing in the output
                  $null = $files.Add($file.FullPath)
                 
                  if($file.IsUntitled)
                  {
                      # untitled files are not yet on disk so we will save the file contents inside the xml
                      # export-clixml performs the appropriate escaping for the contents to be inside the xml
                      $null = $filesContents.Add($file.Editor.Text)
                  }
                  else
                  {
                      # titled files get their content from disk
                      $null = $filesContents.Add($null)  
                  }
              }
              $simpleTab=new-object collections.hashtable
             
              # The DisplayName of a PowerShellTab can only be change with scripting
              # we want to maintain the chosen name       
              $simpleTab["DisplayName"]=$tab.DisplayName
             
              # $files and $filesContents is the information gathered in the foreach $file above
              $simpleTab["Files"]=$files
              $simpleTab["FilesContents"]=$filesContents
             
              # add to the list of tabs
              $null = $tabs.Add($simpleTab)
             
          }
         
          # tabsToSerialize will be a hashtable with all the information we want
          # it is the "root" of the information to be serialized in the hashtable we store...
          $tabToSerialize=new-object collections.hashtable
         
          # the $tabs information gathered in the foreach $tab above...
          $tabToSerialize["TabInformation"] = $tabs
         
          # ...and the selected tab and file.
          $tabToSerialize["SelectedTabDisplayName"] = $psISE.CurrentPowerShellTab.DisplayName
          $tabToSerialize["SelectedFilePath"] = $psISE.CurrentFile.FullPath
         
          # now we just export it to $fileName
          $tabToSerialize | export-clixml -path $fileName
      }
       
       
      function Import-ISEState
      {
      <#
      .SYNOPSIS
          Reads a file with ISE state information about which files to open and opens them
       
      .DESCRIPTION
          Reads a file created by Export-ISEState with the PowerShell tabs and files to open
         
      .PARAMETER fileName
          The name of the file created with Export-ISEState
       
      .EXAMPLE
          Restores current state from c:\temp\files.isexml
          Import-ISEState c:\temp\files.isexml
      #>
       
          Param
          (
              [Parameter(Position=0, Mandatory=$true)]
              [ValidateNotNullOrEmpty()]
              [string]$fileName
          )
         
         
          # currentTabs is used to keep track of the tabs currently opened.
          # If "PowerShellTab 1" is opened and $fileName contains files for it, we
          # want to open them in "PowerShellTab 1"
          $currentTabs=new-object collections.hashtable
          foreach ($tab in $psISE.PowerShellTabs)
          {
              $currentTabs[$tab.DisplayName]=$tab
          }
         
          $tabs=import-cliXml -path $fileName
       
          # those will keep track of selected tab and files   
          $selectedTab=$null
          $selectedFile=$null
       
          foreach ($tab in $tabs.TabInformation)
          {
              $newTab=$currentTabs[$tab.DisplayName]
              if($newTab -eq $null)
              {
                  $newTab=$psISE.PowerShellTabs.Add()
                  $newTab.DisplayName=$tab.DisplayName
              }
              #newTab now has a brand new or a previouslly existing PowerShell tab with the same name as the one in the file
             
              # if the tab is the selected tab save it for later selection
              if($newTab.DisplayName -eq $tabs.SelectedTabDisplayName)
              {
                  $selectedTab=$newTab
              }
             
              # currentUntitledFileContents keeps track of the contents for untitled files
              # if you already have the content in one of your untitled files
              # there is no reason to add the same content again
              # this will make sure calling import-ISEState multiple times
              # does not keep on adding untitled files
              $currentUntitledFileContents=new-object collections.hashtable
              foreach ($newTabFile in $newTab.Files)
              {
                  if($newTabFile.IsUntitled)
                  {
                      $currentUntitledFileContents[$newTabFile.Editor.Text]=$newTabFile
                  }
              }
             
              # since we will want both file and fileContents we need to use a for instead of a foreach
              for($i=0;$i -lt $tab.Files.Count;$i++)
              {
                  $file = $tab.Files[$i]
                  $fileContents = $tab.FilesContents[$i]
       
                  #fileContents will be $null for titled files
                  if($fileContents -eq $null)
                  {
                      # the overload of Add taking one string opens the file identified by the string
                      $newFile = $newTab.Files.Add($file)
                  }
                  else # the file is untitled
                  {
                      #see if the content is already present in $newTab
                      $newFile=$currentUntitledFileContents[$fileContents]
                     
                      if($newFile -eq $null)
                      {
                          # the overload of Add taking no arguments creates a new untitled file
                          # The number for untitled files is determined by the application so we
                          # don't try to keep the untitled number, we just create a new untitled.
                          $newFile = $newTab.Files.Add()
                     
                          # and here we restore the contents
                          $newFile.Editor.Text=$fileContents
                      }
                  }
             
                  # if the file is the selected file in the selected tab save it for later selection   
                  if(($selectedTab -eq $newTab) -and ($tabs.SelectedFilePath -eq $file))
                  {
                      $selectedFile = $newFile
                  }
              }
          }
         
          #finally we selected the PowerShellTab that was selected and the file that was selected on it.
          $psISE.PowerShellTabs.SetSelectedPowerShellTab($selectedTab)
          if($selectedFile -ne $null)
          {
              $selectedTab.Files.SetSelectedFile($selectedFile)
          }
      }
      
      # Define our constant variables.
      [string]$NEW_LINE_STRING = "`r`n"
      [string]$COMMENT_STRING = "#"
      
      function Select-EntireLinesInIseSelectedTextAndReturnFirstAndLastSelectedLineNumbers([bool]$DoNothingWhenNotCertainOfWhichLinesToSelect = $false)
      {
      <#
          .SYNOPSIS
          Exands the selected text to make sure the entire lines are selected.
          Returns $null if we can't determine with certainty which lines to select and the 
      
          .DESCRIPTION
          Exands the selected text to make sure the entire lines are selected.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToSelect
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to select the entire selected lines, but we may guess wrong and select the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be selected.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may comment out the 1st and 2nd lines correctly, or it may comment out the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get selected, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      
          .OUTPUT
          PSObject. Returns a PSObject with the properties FirstLineNumber and LastLineNumber, which correspond to the first and last line numbers of the selected text.
      #>
      
          # Backup all of the original info before we modify it.
          [int]$originalCaretLine = $psISE.CurrentFile.Editor.CaretLine
          [string]$originalSelectedText = $psISE.CurrentFile.Editor.SelectedText
          [string]$originalCaretLineText = $psISE.CurrentFile.Editor.CaretLineText
      
          # Assume only one line is selected.
          [int]$textToSelectFirstLine = $originalCaretLine
          [int]$textToSelectLastLine = $originalCaretLine
      
          #------------------------
          # Before we process the selected text, we need to make sure all selected lines are fully selected (i.e. the entire line is selected).
          #------------------------
      
          # If no text is selected, OR only part of one line is selected (and it doesn't include the start of the line), select the entire line that the caret is currently on.
          if (($psISE.CurrentFile.Editor.SelectedText.Length -le 0) -or !$psISE.CurrentFile.Editor.SelectedText.Contains($NEW_LINE_STRING))
          {
              $psISE.CurrentFile.Editor.SelectCaretLine()
          }
          # Else the first part of one line (or the entire line), or multiple lines are selected.
          else
          {
              # Get the number of lines in the originally selected text.
              [string[]] $originalSelectedTextArray = $originalSelectedText.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
              [int]$numberOfLinesInSelectedText = $originalSelectedTextArray.Length
      
              # If only one line is selected, make sure it is fully selected.
              if ($numberOfLinesInSelectedText -le 1)
              {
                  $psISE.CurrentFile.Editor.SelectCaretLine()
              }
              # Else there are multiple lines selected, so make sure the first character of the top line is selected (so that we put the comment character at the start of the top line, not in the middle).
              # The first character of the bottom line will always be selected when multiple lines are selected, so we don't have to worry about making sure it is selected; only the top line.
              else
              {
                  # Determine if the caret is on the first or last line of the selected text.
                  [bool]$isCaretOnFirstLineOfSelectedText = $false
                  [string]$firstLineOfOriginalSelectedText = $originalSelectedTextArray[0]
                  [string]$lastLineOfOriginalSelectedText = $originalSelectedTextArray[$originalSelectedTextArray.Length - 1]
      
                  # If the caret is definitely on the first line.
                  if ($originalCaretLineText.EndsWith($firstLineOfOriginalSelectedText) -and !$originalCaretLineText.StartsWith($lastLineOfOriginalSelectedText))
                  {
                      $isCaretOnFirstLineOfSelectedText = $true
                  }
                  # Else if the caret is definitely on the last line.
                  elseif ($originalCaretLineText.StartsWith($lastLineOfOriginalSelectedText) -and !$originalCaretLineText.EndsWith($firstLineOfOriginalSelectedText))
                  {
                      $isCaretOnFirstLineOfSelectedText = $false
                  }
                  # Else we need to do further analysis to determine if the caret is on the first or last line of the selected text.
                  else
                  {
                      [int]$numberOfLinesInFile = $psISE.CurrentFile.Editor.LineCount
      
                      [string]$caretOnFirstLineText = [string]::Empty
                      [int]$caretOnFirstLineArrayStartIndex = ($originalCaretLine - 1) # -1 because array starts at 0 and file lines start at 1.
                      [int]$caretOnFirstLineArrayStopIndex = $caretOnFirstLineArrayStartIndex + ($numberOfLinesInSelectedText - 1) # -1 because the starting line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                      [string]$caretOnLastLineText = [string]::Empty
                      [int]$caretOnLastLineArrayStopIndex = ($originalCaretLine - 1)  # -1 because array starts at 0 and file lines start at 1.
                      [int]$caretOnLastLineArrayStartIndex = $caretOnLastLineArrayStopIndex - ($numberOfLinesInSelectedText - 1) # -1 because the stopping line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                      # If the caret being on the first line would cause us to go "off the file", then we know the caret is on the last line.
                      if (($caretOnFirstLineArrayStartIndex -lt 0) -or ($caretOnFirstLineArrayStopIndex -ge $numberOfLinesInFile))
                      {
                          $isCaretOnFirstLineOfSelectedText = $false
                      }
                      # If the caret being on the last line would cause us to go "off the file", then we know the caret is on the first line.
                      elseif (($caretOnLastLineArrayStartIndex -lt 0) -or ($caretOnLastLineArrayStopIndex -ge $numberOfLinesInFile))
                      {
                          $isCaretOnFirstLineOfSelectedText = $true
                      }
                      # Else we still don't know where the caret is.
                      else
                      {
                          [string[]]$filesTextArray = $psISE.CurrentFile.Editor.Text.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
      
                          # Get the text of the lines where the caret is on the first line of the selected text.
                          [string[]]$caretOnFirstLineTextArray = @([string]::Empty) * $numberOfLinesInSelectedText # Declare an array with the number of elements required.
                          [System.Array]::Copy($filesTextArray, $caretOnFirstLineArrayStartIndex, $caretOnFirstLineTextArray, 0, $numberOfLinesInSelectedText)
                          $caretOnFirstLineText = $caretOnFirstLineTextArray -join $NEW_LINE_STRING
      
                          # Get the text of the lines where the caret is on the last line of the selected text.
                          [string[]]$caretOnLastLineTextArray = @([string]::Empty) * $numberOfLinesInSelectedText # Declare an array with the number of elements required.
                          [System.Array]::Copy($filesTextArray, $caretOnLastLineArrayStartIndex, $caretOnLastLineTextArray, 0, $numberOfLinesInSelectedText)
                          $caretOnLastLineText = $caretOnLastLineTextArray -join $NEW_LINE_STRING
      
                          [bool]$caretOnFirstLineTextContainsOriginalSelectedText = $caretOnFirstLineText.Contains($originalSelectedText)
                          [bool]$caretOnLastLineTextContainsOriginalSelectedText = $caretOnLastLineText.Contains($originalSelectedText)
      
                          # If the selected text is only within the text of when the caret is on the first line, then we know for sure the caret is on the first line.
                          if ($caretOnFirstLineTextContainsOriginalSelectedText -and !$caretOnLastLineTextContainsOriginalSelectedText)
                          {
                              $isCaretOnFirstLineOfSelectedText = $true
                          }
                          # Else if the selected text is only within the text of when the caret is on the last line, then we know for sure the caret is on the last line.
                          elseif ($caretOnLastLineTextContainsOriginalSelectedText -and !$caretOnFirstLineTextContainsOriginalSelectedText)
                          {
                              $isCaretOnFirstLineOfSelectedText = $false
                          }
                          # Else if the selected text is in both sets of text, then we don't know for sure if the caret is on the first or last line.
                          elseif ($caretOnFirstLineTextContainsOriginalSelectedText -and $caretOnLastLineTextContainsOriginalSelectedText)
                          {
                              # If we shouldn't do anything since we might comment out text that is not selected by the user, just exit this function and return null.
                              if ($DoNothingWhenNotCertainOfWhichLinesToSelect)
                              {
                                  return $null
                              }
                          }
                          # Else something went wrong and there is a flaw in this logic, since the selected text should be in one of our two strings, so let's just guess!
                          else
                          {
                              Write-Error "WHAT HAPPENED?!?! This line should never be reached. There is a flaw in our logic!"
                              return $null
                          }
                      }
                  }
      
                  # Assume the caret is on the first line of the selected text, so we want to select text from the caret's line downward.
                  $textToSelectFirstLine = $originalCaretLine
                  $textToSelectLastLine = $originalCaretLine + ($numberOfLinesInSelectedText - 1) # -1 because the starting line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
      
                  # If the caret is actually on the last line of the selected text, we want to select text from the caret's line upward.
                  if (!$isCaretOnFirstLineOfSelectedText)
                  {
                      $textToSelectFirstLine = $originalCaretLine - ($numberOfLinesInSelectedText - 1) # -1 because the stopping line is inclusive (i.e. if we want 1 line the start and stop lines should be the same).
                      $textToSelectLastLine = $originalCaretLine
                  }
      
                  # Re-select the text, making sure the entire first and last lines are selected. +1 on EndLineWidth because column starts at 1, not 0.
                  $psISE.CurrentFile.Editor.Select($textToSelectFirstLine, 1, $textToSelectLastLine, $psISE.CurrentFile.Editor.GetLineLength($textToSelectLastLine) + 1)
              }
          }
      
          # Return the first and last line numbers selected.
          $selectedTextFirstAndLastLineNumbers = New-Object PSObject -Property @{
              FirstLineNumber = $textToSelectFirstLine
              LastLineNumber = $textToSelectLastLine
          }
          return $selectedTextFirstAndLastLineNumbers
      }
      
      function CommentOrUncommentIseSelectedLines([bool]$CommentLines = $false, [bool]$DoNothingWhenNotCertainOfWhichLinesToSelect = $false)
      {
          $selectedTextFirstAndLastLineNumbers = Select-EntireLinesInIseSelectedTextAndReturnFirstAndLastSelectedLineNumbers $DoNothingWhenNotCertainOfWhichLinesToSelect
      
          # If we couldn't determine which lines to select, just exit without changing anything.
          if ($selectedTextFirstAndLastLineNumbers -eq $null) { return }
      
          # Get the text lines selected.
          [int]$selectedTextFirstLineNumber = $selectedTextFirstAndLastLineNumbers.FirstLineNumber
          [int]$selectedTextLastLineNumber = $selectedTextFirstAndLastLineNumbers.LastLineNumber
      
          # Get the Selected Text and convert it into an array of strings so we can easily process each line.
          [string]$selectedText = $psISE.CurrentFile.Editor.SelectedText
          [string[]] $selectedTextArray = $selectedText.Split([string[]]$NEW_LINE_STRING, [StringSplitOptions]::None)
      
          # Process each line of the Selected Text, and save the modified lines into a text array.
          [string[]]$newSelectedTextArray = @()
          $selectedTextArray | foreach {
              # If the line is not blank, add a comment character to the start of it.
              [string]$lineText = $_
              if ([string]::IsNullOrWhiteSpace($lineText)) { $newSelectedTextArray += $lineText }
              else 
              {
                  # If we should be commenting the lines out, add a comment character to the start of the line.
                  if ($CommentLines) 
                  { $newSelectedTextArray += "$COMMENT_STRING$lineText" }
                  # Else we should be uncommenting, so remove a comment character from the start of the line if it exists.
                  else 
                  {
                      # If the line begins with a comment, remove one (and only one) comment character.
                      if ($lineText.StartsWith($COMMENT_STRING))
                      {
                          $lineText = $lineText.Substring($COMMENT_STRING.Length)
                      } 
                      $newSelectedTextArray += $lineText
                  }
              }
          }
      
          # Join the text array back together to get the new Selected Text string.
          [string]$newSelectedText = $newSelectedTextArray -join $NEW_LINE_STRING
      
          # Overwrite the currently Selected Text with the new Selected Text.
          $psISE.CurrentFile.Editor.InsertText($newSelectedText)
      
          # Fully select all of the lines that were modified. +1 on End Line's Width because column starts at 1, not 0.
          $psISE.CurrentFile.Editor.Select($selectedTextFirstLineNumber, 1, $selectedTextLastLineNumber, $psISE.CurrentFile.Editor.GetLineLength($selectedTextLastLineNumber) + 1)
      }
      
      function Comment-IseSelectedLines([switch]$DoNothingWhenNotCertainOfWhichLinesToComment)
      {
      <#
          .SYNOPSIS
          Places a comment character at the start of each line of the selected text in the current PS ISE file.
          If no text is selected, it will comment out the line that the caret is on.
      
          .DESCRIPTION
          Places a comment character at the start of each line of the selected text in the current PS ISE file.
          If no text is selected, it will comment out the line that the caret is on.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToComment
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to comment out the selected lines, but we may guess wrong and comment out the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be commented out.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may comment out the 1st and 2nd lines correctly, or it may comment out the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get commented out, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      #>
          CommentOrUncommentIseSelectedLines -CommentLines $true -DoNothingWhenNotCertainOfWhichLinesToSelect $DoNothingWhenNotCertainOfWhichLinesToComment
      }
      
      function Uncomment-IseSelectedLines([switch]$DoNothingWhenNotCertainOfWhichLinesToUncomment)
      {
      <#
          .SYNOPSIS
          Removes the comment character from the start of each line of the selected text in the current PS ISE file (if it is commented out).
          If no text is selected, it will uncomment the line that the caret is on.
      
          .DESCRIPTION
          Removes the comment character from the start of each line of the selected text in the current PS ISE file (if it is commented out).
          If no text is selected, it will uncomment the line that the caret is on.
      
          .PARAMETER DoNothingWhenNotCertainOfWhichLinesToUncomment
          Under the following edge case we can't determine for sure which lines in the file are selected.
          If this switch is not provided and the edge case is encountered, we will guess and attempt to uncomment the selected lines, but we may guess wrong and uncomment out the lines above/below the selected lines.
          If this switch is provided and the edge case is encountered, no lines will be uncommentet.
      
          Edge Case:
          - When the selected text occurs multiple times in the document, directly above or below the selected text.
      
          Example:
          abc
          abc
          abc
      
          - If only the first two lines are selected, when you run this command it may uncomment the 1st and 2nd lines correctly, or it may uncomment the 2nd and 3rd lines, depending on
          if the caret is on the 1st line or 2nd line when selecting the text (i.e. the text is selected bottom-to-top vs. top-to-bottom).
          - Since the lines are typically identical for this edge case to occur, you likely won't really care which 2 of the 3 lines get uncommented, so it shouldn't be a big deal.
          But if it bugs you, you can provide this switch.
      #>
          CommentOrUncommentIseSelectedLines -CommentLines $false -DoNothingWhenNotCertainOfWhichLinesToSelect $DoNothingWhenNotCertainOfWhichLinesToUncomment
      }
      
      
      #==========================================================
      # Add ISE Add-ons.
      #==========================================================
      
      # Add a new option in the Add-ons menu to save all files.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Save All" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Save All",{Save-AllISEFiles},"Ctrl+Shift+S")
      }
      
      $ISE_STATE_FILE_PATH = Join-Path (Split-Path $profile -Parent) "IseState.xml"
      
      # Add a new option in the Add-ons menu to export the current ISE state.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Save ISE State" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Save ISE State",{Export-ISEState $ISE_STATE_FILE_PATH},"Alt+Shift+S")
      }
      
      # Add a new option in the Add-ons menu to export the current ISE state and exit.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Save ISE State And Exit" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Save ISE State And Exit",{Export-ISEState $ISE_STATE_FILE_PATH; exit},"Alt+Shift+E")
      }
      
      # Add a new option in the Add-ons menu to import the ISE state.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Load ISE State" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Load ISE State",{Import-ISEState $ISE_STATE_FILE_PATH},"Alt+Shift+L")
      }
      
      # Add a new option in the Add-ons menu to comment all selected lines.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Comment Selected Lines" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Comment Selected Lines",{Comment-IseSelectedLines},"Ctrl+K")
      }
      
      # Add a new option in the Add-ons menu to uncomment all selected lines.
      if (!($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus | Where-Object { $_.DisplayName -eq "Uncomment Selected Lines" }))
      {
          $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Uncomment Selected Lines",{Uncomment-IseSelectedLines},"Ctrl+Shift+K")
      }
      
      #==========================================================
      # Perform script tasks.
      #==========================================================
      
      # Automatically load our saved session if we just opened ISE and have a default blank session.
      # Because this may remove the default "Untitled1.ps1" file, try and have this execute before any other code so the file is removed before the user can start typing in it.
      if (($psISE.PowerShellTabs.Count -eq 1) -and ($psISE.CurrentPowerShellTab.Files.Count -eq 1) -and ($psISE.CurrentPowerShellTab.Files[0].IsUntitled))
      {
          # Remove the default "Untitled1.ps1" file and then load the session.
          if (!$psISE.CurrentPowerShellTab.Files[0].IsRecovered) { $psISE.CurrentPowerShellTab.Files.RemoveAt(0) }
          Import-ISEState $ISE_STATE_FILE_PATH
      }
      
      # Clear the screen so we don't see any output when opening a new session.
      Clear-Host
      

       

      Hopefully this post makes your ISE experience a little better.  Feel free to comment and let me know if you like this or find any problems with it.  Know of any other must-have ISE add-ons? Let me know.

      Happy coding!

      Setting up keyboard shortcuts to build solutions in MSBuild

      June 1st, 2011 No comments

      One of the greatest benefits of building your solution flies in MSBuild (vs in Visual Studio directly) is that it doesn’t lock up the Visual Studio UI, which can be a huge pain if you have a large solution that takes several minutes (or longer) to build.  Building your solution in MSBuild leaves you free to inspect code in Visual Studio while your solution is building.  The only problem is that to do this you have to open a command prompt and type the command + path every time to build.

      If you want to be able to right-click on a solution file and build it in MSBuild from the Windows Explorer context menu, check out MSBuildShellExtension (it’s free).  Being able to build right from Windows Explorer (without having to even open Visual Studio) is cool and may be enough to passify you, but I wanted to be able to build my solution file at anytime from anywhere on my PC with a keyboard shortcut.

      Below I outline how I’ve setup my system to build my solution files in MSBuild with a quick keystroke.  Setup only takes a few minutes and requires AutoHotkey to be installed (it’s free and awesome).

      Step 1 – Install AutoHotkey.

      Step 2 – Create a shortcut to the Visual Studio Command Prompt (2010), move it directly to the C: drive, and make sure it is called “Visual Studio Command Prompt (2010)” (it is referenced at this location with this name by the AutoHotkey script in the following steps, but can be changed if needed).

      VS2010CommandPrompt

      CDrive

      Step 3 – Create your AutoHotkey script……luckily, you don’t have to create it from scratch; you can use mine as your template and just adjust it to your liking . So copy and paste the script in the textbox below into a new text file, and then save it with the extension ".ahk", which is the AutoHotkey script extension (so you can just call it "AutoHotkeyScript.ahk" for example).  You will need to modify the code directory paths and solution file names in the script to match yours to build your solutions, but I’ve commented the script fairly thoroughly so it’s easy to see what it’s doing.

      In my office we have both a Client solution and a Server solution, so I have the script setup to build the client solution with WindowsKey+C and the server solution with WindowsKey+S. We also work in multiple branches, so I have global variables near the top of the script that I can set to true to quickly switch between Development, QA, and Release branches.  I also have WindowsKey+U configured to open the code directory and WindowsKey+Q to open the database directory.  Obviously you can change the keyboard mappings to your liking; these are just the ones that I prefer.  As a side note here, just be aware that these will override the default windows key shortcuts; so in my case WindowsKey+U no longer opens up the Windows Ease of Access Center window.

      ; IMPORTANT INFO ABOUT GETTING STARTED: Lines that start with a
      ; semicolon, such as this one, are comments.  They are not executed.
       
      ; This script has a special filename and path because it is automatically
      ; launched when you run the program directly.  Also, any text file whose
      ; name ends in .ahk is associated with the program, which means that it
      ; can be launched simply by double-clicking it.  You can have as many .ahk
      ; files as you want, located in any folder.  You can also run more than
      ; one ahk file simultaneously and each will get its own tray icon.
       
      ; Make it so only one instance of this script can run at a time (and reload the script if another instance of it tries to run)
      #SingleInstance force
       
      ;==========================================================
      ; Global Variables - Path settings, customization, etc.
      ;==========================================================
       
      ; Set one of these to &quot;true&quot; to build from the Staging or Release branches, otherwise we'll use the development branch.
      _UsingTFSStagingBranch := false
      _UsingTFSReleaseCandidate := false
       
      ; Specify the Code Folder containing the Solution files to build
      if (_UsingTFSReleaseCandidate == true)
      {
          ; The directory of the current build's Code folder
          _CodeFolder := &quot;C:\dev\TFS\RQ4TeamProject\Release\RQ4\4.2.0\&quot;
      }
      else if (_UsingTFSStagingBranch == true)
      {
          ; The directory of the current build's Code folder
          _CodeFolder := &quot;C:\dev\TFS\RQ4TeamProject\Staging\RQ4\&quot;
      }
      else
      {
          ; The directory of the current build's Code folder
          _CodeFolder := &quot;C:\dev\TFS\RQ4TeamProject\Dev\RQ4\Core\&quot;
      }
       
      ; Path to the database folder
      _DatabaseFolder := &quot;C:\dev&quot;
       
      ; The path to the Visual Studio Command Prompt link
      _VSCommandPromptPath := &quot;C:\Visual Studio Command Prompt (2010).lnk&quot;
      _VSCommandPromptWindowName := &quot;Administrator: Visual Studio Command Prompt (2010)&quot;
       
      ; The position I want the MS Build window to move to when opened
      _MSBuildWindowPositionX := 400
      _MSBuildWindowPositionY := 270
       
      ; The MSBuild command to use
      _MSBuildCommand := &quot;msbuild&quot; ; /verbosity:minimal&quot;
       
       
      ;==========================================================
      ; WindowsKey+C - Build the Client.sln
      ;==========================================================
      #c UP::
       
      ; Make sure the keys have been released before continuing to avoid accidental commands
      KeyWait LWin
      ;KeyWait c
       
      ;BuildSolution(_CodeFolder . &quot;RQ4.Client.sln&quot;)
      BuildSolution(&quot;RQ4.Client.sln&quot;)
       
      return
       
      ;==========================================================
      ; WindowsKey+S - Build the Server.sln
      ;==========================================================
      #s UP::
       
      ; Make sure the keys have been released before continuing to avoid accidental commands
      KeyWait LWin
      ;KeyWait s
       
      BuildSolution(&quot;RQ4.Server.sln&quot;)
       
      return
       
      ;==========================================================
      ; WindowsKey+B - Build the Server.sln then Client.sln
      ;==========================================================
      #b UP::
       
      ; Make sure the keys have been released before continuing to avoid accidental commands
      KeyWait LWin
      ;KeyWait b
       
      BuildSolution(&quot;RQ4.Server.sln&quot;)
      BuildSolution(&quot;RQ4.Client.sln&quot;)
       
      return
       
      ;==========================================================
      ; WindowsKey+U - Open the Code folder
      ;==========================================================
      #u UP::Run %_CodeFolder%
       
      ;==========================================================
      ; WindowsKey+Q - Open the Database folder
      ;==========================================================
      #q UP::Run %_DatabaseFolder%
       
       
      ;==========================================================
      ; Functions
      ;==========================================================
      BuildSolution(solutionPath)
      {
          ; Let this function know that all variables except the passed in parameters are global variables.
          global
       
          ; If the Visual Studio Command Prompt is already open
          if WinExist(_VSCommandPromptWindowName)
          {
              ; Put it in focus
              WinActivate
          }
          ; Else the VS Command Prompt is not already open
          else
          {
              ; So open the Visual Studio 2008 Command Prompt
              Run %_VSCommandPromptPath%
               
              ; Make sure this window is in focus before sending commands
              WinWaitActive, %_VSCommandPromptWindowName%
               
              ; If the window wasn't opened for some reason
              if Not WinExist(_VSCommandPromptWindowName)
              {
                  ; Display an error message that the VS Command Prompt couldn't be opened
                  MsgBox, There was a problem opening %_VSCommandPromptPath%
               
                  ; Exit, returning failure
                  return false
              }
          }
       
          ; Make sure this window is in focus before sending commands
          WinWaitActive, %_VSCommandPromptWindowName%
       
          ; Move the window to the position I like it to be
          WinMove, _MSBuildWindowPositionX, _MSBuildWindowPositionY
       
          ; Set it to the correct directory
          SendInput cd %_CodeFolder% {Enter}
       
          ;MsgBox %solutionPath%  ; Message box to display the Solution Path for debugging purposes
           
          ; Build the solution file
          SendInput %_MSBuildCommand% %solutionPath% {Enter}
           
          ; Return success
          return true
      }
      

      Step 4 – Have your AutoHotkey script automatically start when you login to Windows, so that you don’t have to manually launch it all the time.

      Method 1:

      This method is the easiest, but I discovered it after Method 2 (below).  Simply open up the Windows Start Menu, navigate to the Startup folder within All Programs, right-click on it and choose Open All Users.  Then simply paste a shortcut to your AutoHotkey script in this folder.  That’s it; the script will now launch whenever any user logs into Windows.  If you only want the script to run when you log into Windows (no other users), then just choose Open instead of Open All Users when right-clicking on the Startup folder.

       indexindex2

      Method 2:

      Open the Windows Task Scheduler and create a new Basic Task.  Give it a name and description (something like “launch AutoHotkey script at login”), and then specify to have it run “When I log on”.  Then specify that you want it to “Start a program”, and then point it towards the AutoHotkey script you created in Step 3.  Before you finish the wizard, check off “Open the Properties dialog for this task when I click Finish”.  When that Properties dialog opens up, go to the Conditions tab and make sure none of the checkboxes under the Power category are checked off; this will ensure the script still launches if you are on a laptop and not plugged into AC power.  If you need your script to “Run as admin”, then on the General tab check off “Run with highest privileges”; this may be required for your script to perform certain actions the require admin privileges, so you can check it off just to be safe.

      Open Task Scheduler(1)

       Create Basic Task in Task Scheduler

       

      Basic Task Conditions

      Run Scheduled Task as Admin_2

      And that’s it.  Now you can build your solution file in MSBuild with a quick keystroke from anywhere on your computer.  I have chosen to use the Windows Key for my shortcut keys, but you don’t have to; you can use whatever keyboard shortcut you want.  And feel free to modify the script I provided to do whatever else you want; AutoHotkey is very powerful and can be used for so many things, so be sure to checkout their website for more examples of scripts and what it can do.  For example lots of people use it to automatically spell-correct as they type, or to automatically expand abbreviations (so I could type DS and hit tab, and have it expand to Daniel Schroeder, or type MyAddress and have it put my address in).

      Happy Coding!

      TFS GoToWorkItem VS command and keyboard shortcut

      April 29th, 2011 No comments

      The button to jump directly to a work item by specifying its ID looks to be on the Work Item Tracking toolbar by default in VS / TFS 2010.  This button is not on the toolbar by default in VS / TFS 2008 though.  To add it yourself just go to Tools => Customize, then choose the category Team and the command Go To Work Item…, and you can drag the command into one of your existing toolbars.

      If you want to setup a keyboard shortcut for the command, just go to Tools => Options => Environment => Keyboard, and the command is called Team.GotoWorkItem.  I map it to Ctrl+Shift+/ since Ctrl+/ is the C# keyboard shortcut to search in a file.

      Some Visual Studio 2010 Shortcuts and C# 4.0 Cool Stuff

      April 17th, 2011 1 comment

      A list of some shortcus and new features to VS 2010 and C# 4.0:

      • Default values for parameters
      • Can access parameters by name (i.e. SomeFunction(name: "Dan", age: 26);
      • Can now put Labels on breakpoints and filter the breakpoints, as well as import and export breakpoints.
      • Window => New Window to open up same file in two separate tabs, or can drag the splitter at the top-right corner of the edit window.
      • Edit => Outlining => Hide Selection to collapse any region of code
      • Alt + Mouse Left Drag for box selection instead of line selection, then just start typing; you can also use Alt+Shift+Arrow Keys to do box selection with the keyboard.
      • Alt+Arrow Keys to move current line up/down.  Can also select multiple lines and use Alt+Up/Down to move the whole selection up/down.
      • In NavigateTo search window (Ctrl + Comma) use capitals to search for camel casing (i.e. CE to find displayCustomerEmails) and a space to do an "and" search (i.e. "email customer" would find displayCustomerEmails).
      • Ctrl + I to do an incremental search of a document, then F3 and Shift + F3 to move to next/previous matches.
      • Use snippets to automatically create code and save time.
      • Ctrl + Period to access VS tickler window instead of having to hover over the variable with the mouse.
      • Ctrl + Alt + Spacebar to change VS to suggest variable names instead of auto completing them.
      • Can right click in document to remove and sort using statements.
      • Enum.TryParse() has been added to match a string or number to an enumerated type.
      • Contract.Requires() and .Ensures() to ensure that function conditions are met (at compile time).
      • String.IsNullOrWhitespace(string);
      • Lazy<T> for thread-safe lazy loading of variables.
      • VS => Options => Debugging => Output Window => Data Binding to give more info about errors.
      • Using System.Threading.Tasks for parallel processing.  Parallel.For() and .ForEach
      • PLINQ => myCollection.InParallel().Where(x => …..);
      • New Dynamic keyword type => just like Object except not checked at compile time.
      • Ctrl+Shift+V to cycle through clipboard ring
      • Alt+Ctrl+Down to access tab menu
      • Ctrl+Shift+Up/Down to move between instances of the highlighted variable
      • Ctrl+] to move back and forth between a functions opening and closing braces (i.e. "{" and "}"). This appears to also work in XAML!
      • Alt+Arrow Keys to move current line up/down.  Can also select multiple lines and use Alt+Up/Down to move the whole selection up/down.
      • Rather than selecting a whole line first, just use Ctrl+C or Ctrl+X to Copy/Cut the entire line. You can also use Shift+Delete to delete an entire line without selecting it.