07.28.06
Playing with multiple rows
Update: This post now hosts two commands, the second one performing aligning operations.
Another entry by the LGFT project. This time, we’ll create a command that allows you to quickly fill rows in columnar editing mode in TextMate with expressions depending on an ever increasing number. For instance we will be able to easily create this:
Hey t1here
Hey t4here
Hey t9here
Not that you would ever want something like that
Anyway, first we’ll see the command, then we’ll see the three modes of usage it has, and finally we’ll talk about how the command works.
Here is the code for the command. Set its input to Selected Text with fallback nothing, and its output to Replace Selected Text. Then put this code in:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + '/lib/dialog.rb'
lines = STDIN.readlines
first_line = lines[0]
if first_line =~ /^(\d+)$/ then
m = $1
else
m = Dialog.request_string(:title => "Linear series",:prompt =>"Enter expression")
end
case m
when /^(\d+)$/ then
m = m.to_i
lines.length.times do
puts m
m += 1
end
when /^(\d+):\s+(.*)$/
m, str = $1.to_i, $2
lines.length.times do
puts eval(str.gsub(/\bi\b/, "#{m}"))
m += 1
end
when /^(\d+):(\S.*)$/
m, str = $1.to_i, $2
lines.length.times do
puts str.gsub(/\bi\b/, "#{m}")
m += 1
end
end
Now, chances are a bunch of backslashes are not showing up. In particular, in all the regular expressions, all d,b,s,S should have a backslash in front of them. That’s the command. Now let’s talk about using it.
Suppose you have this:
2
3
4
5
6
and you realize you wanted it to be:
1
2
3
4
5
Then do the following: Make the 2 into 1, and then select this single column of numbers (you’ll need to enter columnar mode to do that). Then run the command, and it should do the rest for you.
A second way of using the command is when having zero-width selection. In other words, suppose we have:
Hey there
Hey there
Hey there
And we want to turn it to the example above. Then we can do the following: Move the cursor right after the t, press shift+downarrow twice, and then press the option key to enter columnar mode. Then run the command. A dialog should pop up, asking you to enter an expression. Type: 1: i**2
This should do it. Basically, the number before the colon is the starting number to be used. There needs to be at least one space after the colon. For the rest of the line: Any occurence of i that is not in a word is replaced by the starting number increased by one for each new line, and then this string is executed as if it were Ruby code, and the result is printed in that row. Ok, I hope that was complicated enough.
Moving on to the third way of using the command. Suppose we instead wanted to produce:
Hey t<2>here
Hey t<3>here
Hey t<4>here
The we would do the same steps as above, except at the prompt would type 2:<i>
So this is the case where there is no space following the colon. Then the rest is inserted as is, except that each i not in a word gets replaced by the appropriate number.
Ok then, time to explain how the command works. The key thing to understand is how TextMate feeds columnar input into the command, and how it interprets the output. It does in some sense what you would expect: The selected text passed to the command contains the text of each row in the columnar selection, separated by newline characters. Similarly, when the command has some output, then TextMate splits this output on the newline characters, and adds each line in the corresponding row. So for instance if the input is the following, with the numbers selected as a column:
123
34566
14
then selected text is passed to the command as the string that has 123 followed by two spaces, then followed by a newline, then 34566, another newline, 14, three spaces and finally one more newline.
The one thing that TextMate does that might be slightly unexpected and irritating, but kind of makes sense, is that it will append spaces to the lines you told it to output, to make them all have the same width. This might often not be your intent. The next section with the aligning command attempts to remedy that.
Anyway, on to the command. It starts simply enough reading the standard input as an array of lines, each of which contains the newline character. This last point is something we’ll have to watch out for in the aligning command.
Next, we read the first line, in order to determine whether we already have a selection of numbers or not. If we do, then we set m to this first number. m will determine in the case loop what happens. If the user has a zero-width selection, then we use the Dialog class (which we loaded via the require command in the second line).
Then we take cases according whether this m is a number of a more complicated expression. The first when close is the case where it was just a number, in which case we create a linear series starting from that number, and so on for the other two cases. The only thing worth noting is the backslashed b appearing in the regexps. That catches i’s that are not part of a word: It stands in particular for “word boundary”. Then eval just evaluates this line as Ruby code and returns the output, which we then print.
You will notice that there is a lot of repetition, namely the times looping that appears three times. It is left as an exercise to the reader to rewrite things a bit to make it shorted/clearer, DRYer.
Aligning
Those of you that played a bit with the above, or read the comments, will have noticed the problem that occurs when the numbers to be inserted have different lengths, namely there is automatic appending of spaces to left-align the numbers, and have columns of equal width. The following command allows you to quickly change between alignment options, kind of in the spirit of what DrDrang suggested:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + '/lib/dialog.rb'
require ENV['TM_SUPPORT_PATH'] + '/lib/exit_codes.rb'
lines = STDIN.readlines
choice = Dialog.request_string(:title => "Realigning things",:prompt =>"Enter align instruction (L,R,R0)")
length = (lines.map {|line| line.length}).max - 1
case choice.upcase
when /^R0/
pattern = "%0#{length}d"
when /^L/
pattern = "%-#{length}s"
when /^R/
pattern = "%#{length}s"
else
TextMate.exit_discard
end
for line in lines do
puts pattern % (line.strip)
end
Set this command to have the same input/output settings as the previous one.
Later
Soryu said,
July 29, 2006 at 5:12 am
Hey Haris, that’s a nice command one always needs and is too lazy to write oneself. A problem though: Have you tried it with numbers > 9? It inserts spaces in that case, to pad the numbers with fewer digits. They are also aligned to the left. I don’t know which would be the best solution, for variable names in programming languages we don’t like spaces and in texts we might at least want the numbers right-aligned, I’d say.
Just my thoughts…
Haris said,
July 29, 2006 at 6:34 am
Hey Soryu,
yeah, I’m afraid that’s how TextMate deals with columnar editing mode. I really don’t want the spaces there either, but I have no control over them. Perhaps right aligning would be better, depends on context I guess.
Dr. Drang said,
July 29, 2006 at 7:27 am
Hey Haris & Soryu,
What about using printf-style strings and zero-padding? Zero-padding usually works out the best for programatic things, and it should be easy enough to strip out the zeros (or replace them with spaces) in other contexts.
I don’t really know Ruby, but it looks like you could use lines.length to determine how many digits to pad out to.
In any event, a neat little program.
Clayton Hynfield said,
August 4, 2006 at 10:54 am
Looks like, in the column alignment script, the value returned by the “Aligning things” dialog is an Array, not a String, so I had to change
to
After that—awesome!
Haris said,
August 6, 2006 at 10:10 pm
Clayton,
actually, this was caused by a bug in the latest update to the dialog.rb file. I fixed that now, so that it returns the string instead of the array, so you should not have to do that any more.