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:
eachis to be used as alleachcommands. 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.coordinatesForNode(node)returns an array with the line and column of where the scope corresponding to thenodestarts.- nodeForTextLocation(location) returns a node matching the smallest scope corresponding to the
location.locationshould be an array containing a line and column number. - upScope(selection, location) returns the node matching the smallest scope at
locationstrictly containing the text in theselection.selectioncould 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). - previousScope(selection, location) returns the node matching the previous scope to the smallest scope in the
locationcontaining theselection. - nodesWithScope(scope) returns an array of all nodes whose
scopeNamecontainsscope, ifscopeis a string, or matchesscopeif `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:
children, an array of all children to the node, corresponding to text nodes as well as nodes of scopes contained in it.parent, the parent node of course. The parent of root is nil.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 ofscopeNameis:text. The value for the root is:root.data, the full text that falls under this scope at this location.coords, an array with the line and column number where this scope starts.
Nodes have the following methods:
first, lastreturn the first (resp. last) child of the node.previous, nextreturn the previous (resp. next) “extended” sibling. This corresponds exactly to the text right before this scope starts.text?returns whether the node is a text node or not.first_child?, last_child?returns whether the node is the first (resp.last) child of its ancestor.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.eachcalls 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 `
. 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 < (resp. >) 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 “
- ”
nodes = handler.find_all{|node| node.scopeName == scope}
for node in nodes
puts ”
- &column=#{node.coords[1]}\”>\ #{node.data.slice(0..60).gsub(”\n”,”\\\\n”)} \n” end 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.