Sunday, September 18, 2011

Autohotkey script for HTML/XML - create closing tags

Note. Get the AHK script. Discuss it on the Autohotkey forum, or see discussion on Autohotkey for the best way to select a word.

UltraEdit is my favourite text editor: it has a very strong macro language and lets you write scripts in Javascript as well. Previously I wrote about a set of UltraEdit macros for creating XML/HTML tags: UltraEdit macro for HTML/XML - auto-create closing tags. I use these two macros so often in UltraEdit that I began to feel the pain whenever I was editing some Google Doc, forum post, or XML file in Eclipse and wanted the same functionality. This is where Autohotkey comes in - I have already written about my favourite Autohotkey scripts. Autohotkey is an amazing Windows scripting language that is flexible and relatively easy to grasp. So I set about translating these two UltraEdit macros into Autohotkey. Here is the result.

The first macro outputs an empty container tag, with the cursor left in the middle. For example, if I were to type out xmlTag| and press Alt+Control+Shift+r, I will be left with the following: <xmlTag>|</xmlTag>. The pipe character (|) indicates cursor position, i.e. when you invoke the macro, your cursor should be at the end of the word (after the "g" in this example) and will be left in between the two tags when the macro finishes. If you have a tag name that contains non alpha-numeric characters such as hyphens, underscores or (Maker forbid) spaces, select the entire tag name first.

; Make XML tag out of current token, leaving cursor in the middle.
; Usage GOOD (selects token with a control+shift LEFT):
;  token|
;  >token< entire "token" selected
;  >token-token< entire "token-token" selected
;  >token_token< entire "token_token" selected
; Usage BAD:
;  token-token|
;  token_token|
;  toke|n
;  |token
!^+r::
   ClipSaved := ClipboardAll  ; Save the entire clipboard to a variable of your choice.
   Clipboard := ; Clear the clipboard
   Send ^x
   if (clipboard == "") {
      Send, {CTRLDOWN}{SHIFTDOWN}{LEFT}{CTRLUP}{SHIFTUP}{CTRLDOWN}x{CTRLUP}
   }
   token := RegExReplace(Clipboard, "[[:space:]]")
   whiteSpace := RegExReplace(Clipboard, "\S")
   Send <%token%></%token%>%whiteSpace%
   x := StrLen(token) + 3 + StrLen(whiteSpace)
   Loop, %x% {
      Send, {Left}
   }
   Clipboard = %ClipSaved%   ; Restore the original clipboard. Note the use of Clipboard (not ClipboardAll).
   ClipSaved =   ; Free the memory in case the clipboard was very large.
return

The second macro does the same thing except that it will paste whatever is in your clipboard in between the two tags and leave the cursor at the end of the closing tag. For example, if I copied the text lorum ipsum baby! and then typed out xmlTag| and pressed Alt+Control+Shift+t, I will be left with the following: <xmlTag>lorum ipsum baby!</xmlTag>|. Again, the pipe character (|) indicates cursor position, i.e. when you invoke the macro, you cursor should be at the end of the word (after the "g" in this example) and will be left at the end of the closing tag when the macro finishes. If you have a tag name that contains non alpha-numeric characters, select the entire tag name first.

; Make XML tag out of current token, pasting clipboard contents in between and leaving cursor after the end of the closing tag.
; Usage GOOD (selects token with a control+shift LEFT):
;  token|
;  >token< entire "token" selected
;  >token-token< entire "token-token" selected
;  >token_token< entire "token_token" selected
; Usage BAD:
;  token-token|
;  token_token|
;  toke|n
;  |token
!^+t::
   tagContents = %clipboard%    ; Convert any copied files, HTML, or other formatted text to plain text.
   Clipboard := ; Clear the clipboard
   Send ^x
   if (clipboard == "") {
      Send, {CTRLDOWN}{SHIFTDOWN}{LEFT}{CTRLUP}{SHIFTUP}{CTRLDOWN}x{CTRLUP}
   }
   token := RegExReplace(Clipboard, "[[:space:]]")
   whiteSpace := RegExReplace(Clipboard, "\S")
   Clipboard = %tagContents%   ; Restore the original clipboard as text.
   Send <%token%>^v</%token%>%whiteSpace%
   x := StrLen(whiteSpace)
   Loop, %x% {
      Send, {Left}
   }
   tagContents =   ; Free the memory in case the clipboard was very large.
   clipLength :=
return

Not as good as UltraEdit

Unfortunately, this Autohotkey script is not as good as the UltraEdit macro - because the UltraEdit macro command SelectWord correctly handles "selecting current word under the cursor" irrespective of whether your cursor is at the start, end or in the middle of the word, or whether the word is at the start, end or somewhere in the middle of a line. In the Autohotkey script, I am using {CTRLDOWN}{SHIFTDOWN}{LEFT}{CTRLUP}{SHIFTUP} (control+shift+left) to select the word just typed (unless you have the entire word already selected). The control+shift+left action yields incorrect results if the cursor is not at the end of the word - and using control+right, control+shift+left yields incorrect results if the word is at the end of the line and the cursor is at the end of the word (because it will select the newline character as well). I don't know how to mimic the SelectWord behaviour properly in Autohotkey without resorting to imitating a double click (which is crazy - your mouse and cursor will be at different positions when you are typing) or using caret logic that won't work in all types of inputs that accept text.

As a result, I use the UltraEdit macros when in UltraEdit and the Autohotkey macros when in any other program - but I still have the same keyboard mappings for both! How do I do this? Because Autohotkey is so cool that you can even define what applications a script should be active for. I surround the Autohotkey script logic in a #IfWinNotActive, UltraEdit directive that will prevent it from working in UltraEdit. See below for an example of this.

#IfWinNotActive, UltraEdit
  ; ... Autohotkey script that will work in any app other than UltraEdit.
#IfWinNotActive

So I get functionality that is more or less the same everywhere, just more flexible in UltraEdit.