The ScopeHandler class

As of revision 948, TextMate now supports the ability to have the entire document together with all the scope information as input for commands. The ScopeHandler class is a Ruby class that offers you an easy interface to this “xml”-style input. This is the help for this class.

_Leave your comments on the blog post created for this purpose: _.Current version: 1.2.1 See the file for version history.

Basic usage

So how do you get to use this class? First, place the source file, scopeHandler.rb somewhere where you can find it. Then create a new command and save it. For the time being, you need to locate the command’s .plist file and edit it, adding the pair of lines:

inputFormat xml

Then you need to restart TextMate. Hopefully some time in the future this will be customizable from within TextMate.

Then set the Input of the command to “Entire Document”, and output “Create new document”. To test your command, just have its text be echocat and try the command out. You should see an xml-style output. Pretty sweet, eh?

Now, on to use ScopeHandler. Change the command to have the following text:

!/usr/bin/env ruby

require ‘/pathto/scopeHandler.rb’ handler = ScopeHandler.new(STDIN.read)

Update: You can now pass to the new method either an IO object, like STDIN, or the string with the data, like in this example.

Now you are good to go! you can ask handler to do things for you. For instance:

location = [ENV[’TMLINENUMBER’].toi,ENV[’TMLINEINDEX’].toi+1] node = handler.upScope(nil, location) print node.data

will return for you the text in the current, closest, scope to the current location of the cursor. If you want to see a bit more information, change the last line to print node. You can get hold of the tree with all the scopes by calling handler.tree. For instance, try print handler.tree.root. That’s the basic stuff. Now, you can go off to explore on your own (get the files here), or read ahead for more info.

General overview of the classes

ScopeHandler

ScopeHandler is initialized by ScopeHandler.new(string), where string is the xml-style input. It then goes on to parse the file and produce a tree with all the available information. This tree is available through the call handler.tree. More about the tree later. It responds to the following methods:

  1. each is to be used as all each commands. Given a block, it executes the block with input each node of the tree, starting from the root. Consequently, all methods of the Enumerable module are also supported.
  2. coordinatesForNode(node) returns an array with the line and column of where the scope corresponding to the node starts.
  3. nodeForTextLocation(location) returns a node matching the smallest scope corresponding to the location. location should be an array containing a line and column number.
  4. upScope(selection, location) returns the node matching the smallest scope at location strictly containing the text in the selection. selection could also be a node, in which case this will essentially return its parent. (strictly speaking, it will return the earliest ancestor corresponding to more text).
  5. previousScope(selection, location) returns the node matching the previous scope to the smallest scope in the location containing the selection.
  6. nodesWithScope(scope) returns an array of all nodes whose scopeName contains scope, if scope is a string, or matches scope if `scope is a regular expression.

The last two methods sound kind of complicated, but their intent is that repeated calls to the corresponding TextMate command would allow the selection of larger and larger chunks of the text, or would allow moving sideways around the text, each time selecting text matching a scope. It was planned to incorporate these in certain macros, but this is not entirely trivial yet for technical reasons.

ScopeTree

This is a tree, each node of which correspond to a scope. root returns the root of the tree. each once again can be used to call a block for each node in the tree in order. That’s really all there is to that.

ScopeNode

This is the class of each node of the tree. Each node has the following attributes:

  1. children, an array of all children to the node, corresponding to text nodes as well as nodes of scopes contained in it.
  2. parent, the parent node of course. The parent of root is nil.
  3. scopeName, the name of the scope as a string, e.g. "text.latex". There are also nodes that correspond only to text, and for these the value of scopeName is :text. The value for the root is :root.
  4. data, the full text that falls under this scope at this location.
  5. coords, an array with the line and column number where this scope starts.

Nodes have the following methods:

  1. first, last return the first (resp. last) child of the node.
  2. previous, next return the previous (resp. next) “extended” sibling. This corresponds exactly to the text right before this scope starts.
  3. text? returns whether the node is a text node or not.
  4. first_child?, last_child? returns whether the node is the first (resp.last) child of its ancestor.
  5. simple?, not_simple? returns whether the node is simple or not. A node is called simple, if it has no children, or if it has one child and that child is simple. Simple nodes correspond to the same text as their children.
  6. each calls a block on the node as well as recursively on all its descendants.

That’s it really. You can now write your own commands that scan through the file and single out useful stuff. Here’s some examples:

Examples of use

Finding all classes in an HTML document

This code would do that:

for classNode in handler.find_all{|n| n.scopeName == “entity.other.attribute-name.html” && n.data == “class”} stringNode = classNode.next until stringNode.scopeName == “string.quoted.double.html” stringNode = stringNode.next end puts stringNode.data end

The for loop locates all nodes corresponding to the word “class”, as in `

bar

. Then it moves through the siblings of the node, until it locates the node corresponding to the string "foo". One could probably have immediately usedputs classNode.next.next`. (There’s a text node between the two, matching the equal sign and any space appearing.) This would have avoided the problems arising if we used single quotes instead of double quotes.

Printing each individual text piece together with the scope closest to it

This code would do that:

for textNode in handler.find_all{|n| n.text?} puts textNode.parent.scopeName puts textNode.data.unescapeScope end

This traverses all text nodes, looking at their parent for the corresponding closest scope, and at their data for the actual text. Note the unescapeScope method. This is a method added to the String class that turns all &lt; (resp. &gt;) into < (resp. >). This was necessary since the “xml”-style output had escaped them. (There is a similar escapeScope method for the inverse process.)

The scope structure

This code would do that:

handler.map do |node| if node.scopeName != :text node.depth.times{ print “\t” } print node.scopeName print “\n” end end

depth is a method for nodes returning the depth of the node.

More examples:

Navigational tool

This command, with output set to HTML, produces a way to go to any part of the current document that shares some scope other than the top one (since they all share that one):

scopes = ENV[’TMSCOPE’].tos.split(” “)[0..-1].uniq puts “\n” for scope in scopes puts “

#{scope}

\n” puts “

” end puts “”

Depending on the various scopes, this could be from completely useless to very interesting.

Well, that’s enough for now. Feel free to suggest your own examples.