MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Scripting Rectangular Selections in BBEdit 7

by John Gruber
01/07/2003

Among the new features introduced by BBEdit 7.0, the most interesting is support for rectangular text selections.

It's a feature that has long been offered in many other text editors (mostly on other platforms), thus, it has been a frequent request from BBEdit users who've seen the feature elsewhere. Rectangular selections make it easier to work with tabular formatted text files. For example, you can easily delete the first 10 characters on every line in a file with one press of the Delete key.

To make a rectangular selection in BBEdit 7, hold down the Option key, then click and drag to select a rectangular region of text. The cursor will change from an I-beam to a plus sign as a visual indication that you are creating a rectangular selection. BBEdit only allows you to create rectangular selections when soft-wrapping is turned off in the front most window. Conversely, while there exists a rectangular selection in the front most window, BBEdit will not allow you to turn on soft-wrapping.

A screenshot says a thousand words:

Screenshot of a BBEdit rectangular selection.
Figure 1.

Support for rectangular selections isn't truly necessary to manipulate columns of text. You can instead use AppleScript or regular expressions (either via BBEdit's Find dialog, or for more complicated actions, with a Perl filter). For example, if you wanted to delete the 11th through 20th characters in every line of text file, you could use the following Perl filter:

#!/usr/bin/perl -wp
s/^(.{10}).{10}/$1/;

Which, of course, leaves out the vast majority of people who don't know regular expressions and don't know Perl or AppleScript. And even if you do know Perl, setting up a custom Perl filter each time you want to slice out a column of text is a bit of a hassle. You may not encounter text in such a format every day, but when you do, rectangular selections are handy indeed.

Once you've seen rectangular text selection in action, it seems both obvious and simple, so you might wonder why it took so long for BBEdit to support it. One reason is that it's not nearly as simple as it looks, especially when you look under the hood using AppleScript.

A regular text selection is just a contiguous range of characters. If you run a simple AppleScript like this:

tell application "BBEdit"
    get selection of window 1
end tell

The result will look something like this:

characters 182 thru 243 of text window 1 of application "BBEdit"

But a rectangular selection is not a contiguous range of characters. It looks contiguous on screen because the Nth characters on each line of text line up vertically. But a text file is not a matrix. It is simply a linear series of characters. To select a rectangle of characters, it is not enough for BBEdit to merely draw a rectangle on screen and select every character within the box.

This becomes obvious if you switch the display font from a monospaced typeface to a proportional one:

Screenshot of a 'rectangular' selection that doesn't look like a rectangle, because the typeface is not fixed-width.
Figure 2.

All of a sudden, the rectangular selection no longer looks like a rectangle. Depending on your choice of font and the width of the characters, it might not even be close. You can also create a non-rectangular, rectangular selection--even with a fixed-width typeface--by selecting characters at the end of lines that are not the same length. See Fig. 3:

Screenshot of a 'rectangular' selection that isn't a rectangle, because it includes lines of unequal length.
Figure 3.

Rectangular Selection Scripting Basics

What actually happens when you make a rectangular selection is that BBEdit creates multiple selections, one for each line of text in the rectangular range. You can see this by running the aforementioned get selection of window 1 script while the front window has a rectangular selection. The result, in AppleScript, will be a list that looks like this:

{
    characters 11 thru 20 of text window 1 of application "BBEdit", 
    characters 42 thru 51 of text window 1 of application "BBEdit", 
    characters 73 thru 82 of text window 1 of application "BBEdit", 
    characters 104 thru 113 of text window 1 of application "BBEdit", 
    characters 135 thru 144 of text window 1 of application "BBEdit", 
    characters 166 thru 175 of text window 1 of application "BBEdit"
}

The difference is that a normal BBEdit selection is a reference to a range of characters, but a rectangular selection is a list of character ranges, one for each line of the rectangular selection. Each element of this list is equivalent to a normal BBEdit selection range.

AppleScript in a Nutshell

Related Reading

AppleScript in a Nutshell
By Bruce W. Perry

You can test whether the current selection is rectangular by checking its class. If it is a list, it's a rectangular selection; if it is a character (or an insertion point), it's a normal selection.

set the_sel to selection of window 1 of application "BBEdit"
if class of the_sel is list then
    display dialog "It's a rectangular selection."
else
    display dialog "It's a normal selection."
end if

You can access each element of a rectangular selection list just like a normal selection range. All you need to do is loop through the items in the selection list. Here's a script that will speak each line of a rectangular selection:

tell application "BBEdit"
    set the_sel to selection of window 1
    if class of the_sel is list then
        -- it's a rectangular selection
        repeat with s in the_sel
            say (s as text)
        end repeat
    else
        -- normal (non-rectangular) text selection
        say (the_sel as text)
    end if
end tell

Another option for dealing with rectangular selections is to tell AppleScript to coerce them to text. If you use this line to get the selection:

set the_sel to selection of window 1 as text

you will always get the selection as a single text object. Each line of a rectangular selection will be delimited by a return. This is, in effect, the same thing that happens when you copy a rectangular selection, then paste it into a new text window.

Since BBEdit returns a rectangular selection in the form of a list when you get the selection, it would make sense that you could pass BBEdit a list if you want to set a rectangular selection. And indeed, that's how you do it. Here's a script that will create a rectangular selection consisting of the second and third characters on the first three lines of the front window.

tell application "BBEdit"
    tell window 1
        select {characters 2 thru 4 of line 1, ¬
            characters 2 thru 4 of line 2, ¬
            characters 2 thru 4 of line 3}
    end tell
end tell

Note that the script specifies characters 2 thru 4, but only characters 2 through 3 are actually selected. This is caused by a bug in BBEdit 7.0.1. It selects one character too few when you create a rectangular selection via the scripting interface. You can see this clearly by running the following:

tell window 1 of application "BBEdit"
    set s to selection
    select s
end tell

This script simply gets the current selection, then re-selects it. It should be a no-op,and it is, when you have a regular selection. But when you have a rectangular selection, the selection will shrink by one character width each time you run the script.

The bug has been reported to Bare Bones, but until it's fixed in a future version of BBEdit, you can work around it by specifying one extra character when creating rectangular selections via AppleScript. This workaround is not perfect, however: it will fail if by adding an extra character, you end up specifying a character offset that does not exist. For example, let's say you want to create a rectangular selection consisting of characters 3 through 4 of the first two lines. To compensate for this bug, you might try:

tell window 1 of application "BBEdit"
    select {characters 3 thru 5 of line 1, ¬
        characters 3 thru 5 of line 2}
end tell

This work in some cases, but the reference to characters 3 thru 5 will fail if any of the two lines are only four characters long. You can avoid this by avoiding line-relative character offsets, and instead using window-relative character offsets:

tell window 1 of application "BBEdit"
    select {characters 3 thru 5, ¬
        characters 8 thru 9}
end tell

Even this technique is not perfect, however -- it still will not allow you to create a rectangular selection that includes the last character of the window.

Unsupported: Non-Contiguous Selections

Now things start to get interesting because scripts that create rectangular selections work regardless if the front most BBEdit window is soft-wrapped.

Let's say you have a file with three lines of text. With soft-wrapping turned off, after running the above script, you see this:

Screen shot.
Figure 4.

But if you turn soft-wrapping on (before making the rectangular selection, since BBEdit won't allow you to toggle soft-wrapping if a rectangular selection already exists), then run the script, you see this:

Screen shot.
Figure 5.

The same characters are selected, but because soft-wrapping is on, they no longer form a rectangle. That's just it. They never really were a rectangle in the first place. They just look like a rectangle when soft-wrapping is off and the display font is a fixed-width typeface.

We already know that BBEdit's scripting interface deals with rectangular selections not as rectangular regions, but as lists of character ranges. It ends up that only BBEdit's graphical interface enforces the restriction that rectangular selections must be rectangular regions of text in non-soft-wrapped windows. The scripting interface has no such restrictions and will accept any list of character ranges, regardless if the window is soft-wrapped or if the character ranges form a rectangle.

In other words, via the scripting interface, BBEdit allows you to create multiple, non-contiguous text selections.

A word of caution, however. The only officially supported form of discontiguous selection in BBEdit 7.0 are the rectangular selections offered by the graphical interface. The following scripts work in BBEdit 7.0.1 but could very well break in the future if Bare Bones changes the scripting interface to enforce the same restrictions as the graphical interface. Until and if Bare Bones Software officially supports these techniques, they should not be counted on for production purposes.

Via the scripting interface, you can select ranges with different offsets on each line:

tell window 1 of application "BBEdit"
    select {characters 2 thru 4 of line 2, ¬
        characters 4 thru 6 of line 3, ¬
        characters 6 thru 8 of line 4}
end tell

You can even select multiple ranges of characters on the same line:

tell window 1 of application "BBEdit"
    select {characters 1 thru 4 of line 1, ¬
        characters 11 thru 16 of line 1}
end tell

Screen shot.
Figure 6.

If you tell BBEdit to select adjacent character ranges, BBEdit will merge them into a single regular selection range. If you run this script:

tell window 1 of application "BBEdit"
    select {characters 1 thru 2, ¬
        characters 3 thru 4, ¬
        characters 2 thru 3}
    get selection
end tell

the return value is not a list, but instead a single character range: characters 1 thru 3 of text window 1 of application "BBEdit".

Select All Matches

Arbitrary multiple selections look cool, but are they useful? Other editors have offered multiple selections for a long time, but in my experience, most of them have been styled text editors or word processors--Nisus Writer, for example. Multiple selections have obvious utility in a styled editor or word processor. For example, you could select a bunch of discontiguous words, then change their font, color and size all at once.

That's not something you can do in a plain text editor like BBEdit. However, I can think of several ways that discontiguous selections can be put to use in BBEdit. For example, the following AppleScript will prompt you for a grep pattern, then select all matches of that pattern in the front window. Normally when you perform a "find all" in BBEdit, the results are displayed in a two-paned results browser, with a list of matches on top and the text of the document displayed underneath. With the following script, you can see all matches of the pattern selected at once.

property search_pat : ""

tell application "BBEdit"
   activate
   set w to text window 1
   set search_opts to {search mode:grep, starting at top: ¬
      true, case sensitive:false, returning results:true}
   set search_pat to text returned of (display dialog ¬
      "Select all text matching grep pattern:" default answer search_pat)
   set search_result to find search_pat searching in {text 1 of w} ¬
      options search_opts

   if found of search_result is true then
      set match_list to {}

      -- last_char is used in the bug workaround below
      set last_char to offset of last character of w

      repeat with cur_match in found matches of search_result
         set start_char to (start_offset of cur_match)
         set end_char to (end_offset of cur_match)

         -- start workaround for BBEdit 7.0.1 bug
         if end_char is less than (offset of last character of w) then
            set end_char to end_char + 1
         end if
         -- end workaround

         copy (characters start_char thru end_char of text window 1) ¬
            to end of match_list
      end repeat
      tell w to select match_list
   else
      beep -- no matches
   end if
end tell

Here's how it works.

First, we create a property called search_pat. Properties are similar to normal AppleScript variables, but their values are remembered after the script finishes executing. Thus, each time you invoke the Select All Matches script, the search pattern will default to the string you last searched for.

Next we create a variable to refer to the front most BBEdit window (simply because it's so much shorter to type "w" than "text window 1"), set the search options, and prompt for the search pattern.

Then we tell BBEdit to perform the search. To perform a batch search in BBEdit via AppleScript, you need to pass a list as the search target. By putting "{text 1 of w}" inside curly braces, we create a list, albeit a list with a single element. If we had omitted the curly braces, BBEdit would have performed a normal search, finding only the first occurrence of the pattern.

Mac OS X Hints

Related Reading

Mac OS X Hints
The 500 Most Amazing Power Tips
By Rob Griffiths

Next we check the found property of the search result to see if the search was successful. If it was not, we beep and the script ends.

If the search was successful, we create an empty list named match_list. This is a list we'll construct with the character offsets of the text we want BBEdit to select. We also create a variable named last_char that holds the numeric offset of the last character in the window. last_char is used later on in the script as part of a workaround for the off-by-one selection bug mentioned earlier in this article.

Next we loop through each of the found matches. With each match, we copy the starting and ending offsets. As a workaround for the off-by-one bug, we add 1 to the ending offset, but only if doing so won't refer to an offset larger than that of the last character in the window. (We cache the value of the last character's offset in the last_char variable as a small performance tweak, referring to "offset of last character of w" each time through the repeat loop incurs a small amount of overhead.)

At the end of the repeat loop, we copy a reference to the matching characters to the match_list variable. It is essential to use the copy reference to list syntax to append each reference to the list, rather than using AppleScript's "&" concatenation operator. If we had instead used something like this:

set match_list to match_list & (characters start_char thru ¬
	end_char of text window 1)

we would have created a list of text strings, such as {"One", "Two", "Three"}, rather than a list of references to text objects. See Apple's developer documentation regarding AppleScript lists for more details.

Finally, after the repeat loop finishes, we tell BBEdit to select the elements of match_list, and we're done.

John Gruber is a freelance writer, Web developer, designer, and Mac nerd. He combines those interests on his Web site, Daring Fireball.


Return to the Mac DevCenter.


Copyright © 2009 O'Reilly Media, Inc.