AppleScript scripts for paragraph navigation and selection in BBEdit

Posted by Pierre Igot in: Macintosh
August 23rd, 2012 • 5:04 pm

This is (I hope) the final instalment in my series of posts on customizing the BBEdit user interface in order to allow paragraph selection using the standard OS X shortcuts, i.e. shift-option-Up and shift-option-Down. You can read the first two instalments here:

Thanks to BBEdit user Christopher Stone’s on-going support and investigations, we have been able to determine that the outstanding problem that I was experiencing with my script for extending the selection upwards was not due to something weird and specific to my personal user environment in OS X, but to a combination of factors, namely the presence of some specific characters in my text document (especially characters used in HTML markup) and use of OS X’s key repeat in combination with a Keyboard Maestro macro command using the “is down” trigger parameter.

In other words, because of the interplay between BBEdit, AppleScript, and Keyboard Maestro, we are, in some cases, dealing with substantial system event syncing issues, which explain the hiccups and slow-downs in the execution of repeated instances of the macro in BBEdit in certain text documents.

In any case, Christopher came up with a brilliant, obvious solution, which is not to rely on Keyboard Maestro’s “is down” trigger parameter to keep the key repeat in sync. Instead, he suggested using BBEdit’s built-in keyboard shortcuts feature (in its preferences) to assign obscure keyboard shortcuts to the AppleScripts for extending the selection paragraph by paragraph upwards and downwards, and then using Keyboard Maestro simply to intercept the standard shift-option-Up and shift-option-Down shortcuts that BBEdit’s keyboard shortcuts feature does not let you use and use them as triggers to trigger the appropriate obscure keyboard shortcuts in BBEdit.

This approach bypasses the key repeat issues with no system event syncing problems, and I can still use the standard OS X shortcuts, with or without repeating.

Finally, once I got the shortcuts of extending the selection paragraph by paragraph working, I also wanted the equivalent shortcuts for moving the insertion point paragraph by paragraph to work with the standard OS X behaviour in BBEdit. So I adapted the two scripts to move the insertion point instead of extending the selection.

I still encountered one more problem, which was that the script for moving the insertion point by one paragraph downwards would fail if I was already in the last paragraph of the document, ostensibly because of problems with the length of the selection and the use of a command to place the insertion point after the selection. So I ended up having to add a try… on error… end try clause to my script, and using a rather ugly bit of GUI scripting in case of an error.

To recap, here is the script that I have for extending the selection downwards by one paragraph:

tell application "BBEdit"
	tell text of front text window
		find "[^\\r]+" options {search mode:grep, starting at top:false, wrap around:false, backwards:false, case sensitive:false, match words:false, extend selection:true} with selecting match
	end tell
end tell

I assigned the shortcut command-option-shift-F7 to the script in BBEdit’s preferences, and then I created a Keyboard Maestro macro with the trigger option-shift-Down whose sole action is to trigger the command-option-shift-F7 shortcut. I made sure the parameter for the trigger in Keyboard Maestro was “is down” and not “is pressed”, in order to ensure that it would repeat properly. (Since Keyboard Maestro is the application handling the repeating, it is not necessary to check the option to “Allow meny key equivalents to autorepeat” in BBEdit’s preferences. But it does not hurt either. Effectively what Keyboard Maestro does is that it detects the “down” position of the key and mimics repeated pressing of the shortcut, so to BBEdit it looks like repeated instances of the shortcut and not continuous pressing of the keys.)

Similarly, for extending the selection upwards by one paragraph, I used this script:

tell application "BBEdit"
	tell text of front text window
		set textSelection to selection
		set _selProp to properties of textSelection
		set endMarker to (characterOffset of textSelection) + (length of textSelection) - 1
		set fRec to find "[^\\r]+" options {search mode:grep, backwards:true, returning results:true} with extend selection
		if found of fRec ? false then
			set startmarker to characterOffset of fRec's found object
			select (characters startmarker thru endMarker)
		end if
	end tell
end tell

I assigned the shortcut command-option-shift-F8 to the script in BBEdit’s preferences, and then I create a Keyboard Maestro macro with the trigger option-shift-Up whose sole action is to trigger the command-option-shift-F8 shortcut. Again, I made sure the parameter for the trigger in Keyboard Maestro was “is down” and not “is pressed”.

For moving the insertion point upwards by one paragraph, I used this script:

tell application "BBEdit"
	tell text of front text window
		set textSelection to selection
		set _selProp to properties of textSelection
		set endMarker to (characterOffset of textSelection) + (length of textSelection) - 1
		set fRec to find "[^\\r]+" options {search mode:grep, backwards:true, returning results:true} with extend selection
		if found of fRec ? false then
			set startmarker to characterOffset of fRec's found object
			select insertion point before character startmarker
		end if
	end tell
end tell

It’s very similar to the script for extending the selection, except that afterwards I move the insertion point to the beginning of the new selection, which deselects the selection and moves the insertion point to the desired location. Again, I assigned an obscure shortcut to the script in BBEdit, and then created a macro in Keyboard Maestro with the standard option-Up shortcut (with “is down”) to trigger the obscure shortcut.

And finally, for moving the insertion point downwards by one paragraph, I used this script:

try
	tell application "BBEdit"
		tell text of front text window
			find "[^\\r]+" options {search mode:grep, starting at top:false, wrap around:false, backwards:false, case sensitive:false, match words:false, extend selection:false} with selecting match
			set SelEnd to (characterOffset of selection) + (length of selection)
			select insertion point after character SelEnd
		end tell
	end tell
	
on error
	tell application "System Events" to tell process "BBEdit"
		key code 124
	end tell
end try

I assigned an obscure shortcut to the script in BBEdit, and then created a macro in Keyboard Maestro with the standard option-Down shortcut (with “is down”) to trigger the obscure shortcut.

As indicated, in this last script I had to add a try… on error… end try clause because otherwise the script would fail with an error when the insertion point was already in the last paragraph of the document. Now, in that situation, there is still an error, but the script handles it itself and triggers a key code (equivalent to the right arrow) that deselects the selection and moves the insertion point to the end of it. It’s a bit ugly (you can see the selection flashing for a fraction of a second), but it works. Someone with more expertise could probably come up with something more elegant, but it works for me.

So there we are. I now have four shortcuts (option-Down, option-Up, option-shift-Down, and option-shift-Up) that work in BBEdit more or less like they do in other text editors and word processors, i.e. using the concept of “paragraph” instead of the default behaviour in BBEdit, which uses the concept of “screen”.

It’s probably not perfect, because the scripts rely on grep patterns that are based on a concept of paragraph that might not exactly match the one that OS X uses. (It depends on how exactly you treat the final return char in the paragraph, and whether you go to the end of one paragraph or the beginning of the next one, and vice versa.) But in that department, things are not consistent across all OS X applications anyway, since developers like Microsoft and Adobe use their own non-standard concept of a paragraph too.

The essential thing for me is that I can use my muscle memory to select paragraphs pretty much the same way that I do in other applications, and that’s all that really matters.

Thanks a bunch to Oliver Taylor for prompting me to explore this and to Christopher Stone for his great help. I wouldn’t have been able to create this solution without them (and without, of course, the ever-indispensable Keyboard Maestro).

UPDATE: Christopher Stone, once more, comes up with better scripts to moving the insertion point up:

try
 tell application "BBEdit"
   tell text of front text window
     find "^(?!\\r)" options {backwards:true, case sensitive:false, search mode:grep, wrap around:false} with selecting match
   end tell
 end tell
end try

and down:

try
 tell application "BBEdit"
   tell text of front text window
     find "^(?!\\r)" options {backwards:false, case sensitive:false, search mode:grep, wrap around:false} with selecting match
   end tell
 end tell
end try

Of course they work better than mine :-).

And one other thing to keep in mind is that, whatever scripts you use, your mileage with the key repeat will vary, because it depends on how fast Keyboard Maestro repeats the action. On my machine, I find that, if I hold the keys down for too long, Keyboard Maestro accumulates a pretty large buffer of repeated keystrokes that continue to trigger repeats long after I release the keys.


Comments are closed.