The (Unofficial) BBEdit Hints Weblog
Using (Global) Node Modules in BBEdit Text Filters

Node modules in BBEdit: The Background

Today I integrated a Node module in a BBEdit Text Filter I was writing.

There are a few people who have integrated Node with BBEDit (Brent Simmons and Tim Whidden), but these scripts show the basics.

In my work I was integrating a node module that I had installed via npm install -g. I needed to write some Javascript code to read BBEdit input from STDIN (like you do with a text filter), do a transformation, call the node module and echo that back to BBEdit.

This is easier if the Node module includes a binary. Modules like Coffeescript are easy to interact with: just call their command line tools. Writing a Node program that calls a globally installed module is harder.

Step 1: A shared script to set the paths right (npm_paths)

Node has a declared list of places it will search for modules. But of course where npm installed the modules for me isn’t on that list.

It’s possible Node wasn’t placing globally installed modules in one of the default places because of how I installed Node (via NVM). I have no idea what happens if you install Node via Homebrew or Macports.

Anyway, a 100% sure way to get Node to find your modules is to include them in the NODE_PATH.

None of this matters if you’re using a binary like coffee, but if you’re in a Node.js script yourself and using require to access a module stored in… well, whereever the Node Package Manager stores modules, you’re going to have a bad time. Unless you hard code the module path, and that’s not very charitable.

So I created npm_paths to set the paths correctly:

#!/bin/sh

# It's possible that $NODE_PATH isn't defined, and/or we need to
# ensure that NODE_PATH includes our global module folder so people
# can install modules there and call them from BBEdit scripts
#
# This could be because of the slightly non-standard way I've installed Node
# (NVM - https://github.com/creationix/nvm)
# but I'm not convinced. WD-rpw 03-27-2014
#

export NPM_BIN=$($SHELL -i -c 'which npm')
# let user's shell, with config options tell us where npm is. May be
# installed via homebrew, MacPorts, NVM, etc, so ask

export NODE_BINARIES_PATH=$(PATH=$PATH:`dirname $NPM_BIN` $NPM_BIN bin -g)
# ^^^^^^^ avoids "npm not in path" errors. WD-rpw 03-25-2014

export NODE_BIN="$NODE_BINARIES_PATH/node"
export GLOBAL_NODE_MODULES_PREFIX=$($NPM_BIN config get prefix)
export NODE_PATH=$NODE_PATH:$GLOBAL_NODE_MODULES_PREFIX/lib/node_modules/

Notice how it sets NODE_PATH and also gives us some good hooks to know both where node and npm is.

Also notice how we ask npm itself where it installed things, instead of assuming “it must be in ~/.node_modules

Step 2: A shim script (node_runner)

Now that we have a script to tell us where Node is, we need yet another script to execute the previous script and use those environmental variables to run the appropriate Node.

#!/bin/sh

RESOURCES_PATH=$(dirname $0)
source $RESOURCES_PATH/npm_paths.sh
# ^^^^ this script is in the same folder as npm_paths

$NODE_BIN "$1"

Step 3: Your Script

My script was to analyze specificity of my CSS selectors. I can do that with the clear-cut node module: just call its API with a value and out comes how specific your CSS selector will be.

Enough web design talk.

#!/usr/bin/env ../../Resources/node_runner.sh

specificity = require('clear-cut').specificity

process.stdin.resume();
process.stdin.setEncoding('utf8');

process.stdin.on('data', function(chunk) {
  var withoutNewLine = chunk.trim()

  process.stdout.write(withoutNewLine)  
  process.stdout.write(" /* = ")
  process.stdout.write(String(specificity(chunk)))
  process.stdout.write(" */ ")
  process.stdout.write("\n")
});

Notice how I placed node_runner two folders in a folder named Resources. Inspiration from Dr Drang.

Because we’re using env to call the script we can use relative paths. node_runner will source our npm_paths script, with interrogates the system about where Node/NPM is, in addition to setting up NODE_PATH (essential in the complex case, overkill in a simple case).

Conclusion

Yes, there are a fair number of moving parts going on here. However, having a script like this also gives us some opportunity: keeping each Package’s node dependancies in node_modules in the root of the Package, for example.

Anyway, a technique like this may get you out of a tight spot.

Untitled Text 212 and BBEdit’s launch behavior

I use BBEdit a lot. So much so that I created a new document in BBEdit just now and it’s “Untitled Text 212”.

I’m serious.

Let’s talk about BBEdit’s launch and new document behavior for a second:

BBEdit will restore unsaved changes in documents when you quit and relaunch BBEdit. In the Preferences window go to Application -> Reopen documents that were open at last quit if you want to turn this off.

If you want this most of the time, but not this session, hold down the shift key while launching BBEdit. (Be fast, especially on monster machines: machines with solid state drives open apps fast!)

Holding the shift key down will also reset the untitled text document counter to 0.

Node.js / Express in BBEdit — navigation using markers

BBEdit’s Javascript (or Coffeescript) support is OK, but there are some nice things developers using Express would like.

For example, I’d like to be able to navigate my .get or .post callback blocks in the function menu.

Given how the language menu works in BBEdit this is a reasonably hard task. But an easy work around is to use Markers.

Here’s a pattern you can use with Find And Mark All:

\w*\.((get|post|put|delete)\W*".+")

Set that to the find pattern and \1 to the Mark With pattern and you’ll be all set.

You can also save this pattern in BBEdit -> Setup -> Patterns

Creating Jigs with BBEdit

Yesterday I was writing this code over and over and over:

%li

  = f.label :first_name, "What is your first name, good sir"

  = f.text_area :first_name

The process I needed to do was:

  1. Add a field to the database
  2. Write those three lines of code that referenced the field I created.

I had about 40 fields to create. Half way through my task I realized I could use a Jig.

A jig in woodworking is a custom tool created to control the location of another tool.

In our case it’s a temporary tool created by us, used by our text editor to automate a task. But I write this jig with the intention that I’ll throw it away and never use it again.

After I came to terms with creating something, using it 20 times then throwing it away, I wrote the following BBEdit clipping.

%li

  = f.label :#CLIPBOARD#, "<##>"

  = f.text_area :#CLIPBOARD#

My process for using this jig was:

  1. Create the field in the database, and copy the new field name to the clipboard.
  2. Run the jig. My database field name is now populated
  3. Start typing the desired custom label

My initial problem creating this jig was that I needed the same value two places. Clippings don’t let you mirror one value to another place, so I thought I was stuck, until I realized that I could use the clipboard here.

Sometimes automation happens at a strategic level: “I know I do this a lot, and I know I’m going to do this a lot in the future, so let me let my editor help me”. Sometimes automation happens at a tactical level: “I need to do this 40 times, but then I’ll never do this again”. Both are equally valid!

I put this clipping in its own folder (“Jigs”) and gave it a keyboard shortcut (F10). The next time I need to create a jig I’m going to put it in that same folder and reuse the keyboard shortcut.

Zooming: A feature hidden in Applescript

For a long time I’ve wanted to zoom in my documents, either to see them when really far away or to use them with a projector or Airplay.

Today I found how to do it with BBEdit, thanks to a feature in BBEdit hidden in the Applescript support.

tell application "BBEdit"
    set display magnification of window 1 to 2.0 -- displays text at 2x
end tell

Woh, suddenly my text is bigger!

(set it back to normal by set display magnification of window 1 to 1.0)

"Yeah, but why don’t you just make the font size bigger?"

I investigated the differences between just increasing the font and using the display magnification. They look pretty much identical to me. So, yes, you could increase the font size.

The problem I always have with font size is remembering what size it was originally. Was it 10? 12? 14? (Yes, this is a silly excuse)

With the display magnification property I know the “normal” size is 1.0.

However, yes, it’s mostly a curiosity, which is probably why it’s hidden in the Applescript dictionary and not in the interface somehow.

However, we can change that

Let’s create three Applescripts: Zoom in, Zoom Out, and Zoom to Normal.

Zoom in.applescript

tell application "BBEdit"
    tell window 1
        set display magnification to display magnification * 1.25 
    end tell
end tell

Zoom Out.applescript

tell application "BBEdit"
    tell window 1
        set display magnification to display magnification / 1.25 
    end tell
end tell

Zoom To Normal.applescript

tell application "BBEdit"
    set display magnification of window 1 to 1.0 -- displays text at 2x
end tell

If we add these to the Scripts folder (normally in `~/Library/Application Support/BBEdit/Scripts/) they show up in BBEdit’s Scripts menu.

Finding and selecting text with Applescript

Today I needed to do a find with Applescript then select everything except the first and last character of that selection.

Even if you’re not doing this exact thing, it’s sometimes not obvious in Applescript and BBEdit how to do this task. (It took me about an hour of head scratching to get this script like it is).

To some extent I’ve already covered this topic BBEdit Hints: Selecting a Sentence, but the length of that script obscures the point a bit. This is a simple script that in 6 lines of code does a simple job.

Example of how to create a BBEdit text filter with Applescript

In addition to creating BBEdit text filters with Unix shell tools, you can also create them with Applescript. The User manual for BBEdit talks about this, but lacks an example.

So here’s an example, which uses Applescript to replace every space it finds with a carriage return.

Send Selection to BBEdit Worksheet

Today I wanted to send my selection to the Unix Worksheet, so I wrote a script.

The long and short of it was that I typed a Unix command in a BBEdit text window. I wanted to run this command, but I’m too lazy to copy/paste it to a Worksheet. So I wrote the following script.

tell application "BBEdit"

    set output to selection as text

    set uws to Unix worksheet window
    tell uws
        select insertion point after last character
        set selection to output
    end tell
end tell

And there you have it - send a Unix command to the Worksheet, all selected and ready to run!

Keep your function definitions (ctags) up to date automatically, with guard

I love using ctags in my BBEdit projects so I can quickly jump to the definition of a function. I’ve been manually doing this (using a feature of the Ryan’s Rails package… but today I decided I needed some automation in my life.

Enter guard a Ruby tool that watches a directory for specified files to change, and when they do guard runs a specified command.

There’s a great screencast about Guard that covers many things about it, but if you just want your ctags to be updated automatically here’s what you do.

$ sudo gem install guard
$ sudo gem install fs-notify
$ sudo gem install guard-shell  

For those of you in the Ruby community that Know Better… feel free to omit the sudo, or put the gems in your Gemfile, or RVM global gemset, or whatever. These instructions assume the reader has no experience with Ruby, and no patience to fight with modern professional gem/ruby management techniques.

Create a .Guardfile_rails, with the following contents. Create this in your home directory.

guard :shell do
  watch(%r{^(?:app|lib)/.+\.rb$}) { `/usr/local/bin/bbedit --maketags` }
end

The first parameter to the watch function is a regex matching what files to watch. Since this is a Rails project watching .rb files in app/ or lib/ is fine, as that’s where most of the code lives anyway. You may need to modify this for your particular project requirement.

Guard is supposed to automatically look for and load a .Guardfile in your home directory, if one doesn’t already exist your current project folder. This didn’t work for me. I also write things beyond Ruby some times, so I may want to create a .Guardfile_python.

On the command line, in your project, folder

$ guard --guardfile ~/.Guardfile_rails

Guard will just sit there with a prompt and that’s OK. Save a file in your project and switch back to Guard: it ran the bbedit --maketags script.

That’s all there is to it!

Open in BBEdit via the Shell Worksheet

While BBEdit has a great Open File by Name feature, something I like to do is use the BBEdit Worksheet to open files.

Here’s how:

  1. Open the BBEdit worksheet. I like to use projects, so I use the project specific BBEdit Worksheet
  2. cd into the project directory
  3. Use the bbedit command to open the documents you require.

For example, in Rails if I want to open HTML source for a web page (called views in Rails nomenclature)

bbedit app/views/widgets/new.*

This will open up all the files that begin with new, which might be useful. Rails, for example, has separate files for each format the app outputs data - using the Worksheet in this manner will open them all.