Troublingly, I’ve taken very few notes during lectures at Flatiron. How will I remember niche methods and strange, counter-intuitive syntaxes?!
Of course, we do have awesome labs that we can reference anytime. Whenever I have needed to remember how I used inject
that one time, or how to set up a module, I just remember which lab we did that in. The problem with this approach, of course, is that as the course progresses my labs directory has grown seemingly exponentially. How are we supposed to remember which lab has that great example of a rake test?
Since it’s important for a programmer not to remember anything he or she doesn’t absolutely have to, I figured it’d be helpful to build a little search app that would return snippets of code based on a user’s query.
So! I’ve put together a little Ruby app I’m calling Recall and pushed it up to Github. You can find the public repo here (I actually referred to it in a blog post earlier this week).
What is Recall?
Basically it’s a command line interface to search the .rb files of your local code directory for a phrase– say a method you don’t quite remember how to use but know you’ve used before. It dumps the output into a new .rb file and immediately opens that file with your default .rb editor (Sublime Text 2 for me).
It’s really not much more than a formatted grep search. But in the week that I’ve been working on it on-and-off I’ve used it a handful of times and it’s actually helped! So I decided to push it to GitHub. Hopefully it will be heplful for you too!
Again, here’s a link to the public repo on Github.
Some Interesting Bits From the Code
1. Prepping the Query
The program takes in the user’s query through a gets
in the runner. Then, in the results model, we take the @query
through a series of small methods before running the grep search. Behold!
def method?
@query[0] == '.'
end
def symbol?
@query[0] == ':'
end
def format_query
if method?
a = @query.split('')
a[0] = '\.'
@query = a.join('')
end
if !symbol? # For some reason queries starting with ':' did not
# jibe well with the regex below
c = @query.split('')
c.unshift('\b')
c.push('\b')
@query = c.join('')
end
end
def get_grep_results
format_query
return `grep -r -n -i --include=*.rb "#{@query}" /Users/samschlinkert/Documents/code/flatiron | sort -r`
end
Sandi Metz would be proud (I hope?)! I tried to keep to the Single Responsibility Principle– that is, ensuring each method does one thing. For example, the format_query
method escapes the ‘.’ if the user searches for a method, and it puts ‘\b’ (regex for word border) on either side of the query (unless it’s a symbol… for whatever reason the ‘\b’s didn’t work with symbols). OK, maybe that method does two things but you get the point.
2. The Struct
I’ve actually been reading Metz’s Practical Object Oriented Design in Ruby (aka POODR) this week, so I was inspired to use a Struct in the next portion of the results model.
# define the struct
Result = Struct.new(:file_path, :line_number, :code_snippet, :full_code)
def parse_results
results = get_grep_results.split("\n")
results.map do |result|
line_array = []
line_array = result.split(":")
Result.new(line_array[0], line_array[1], line_array[2], [''])
end
end
def get_full_snippet
results = parse_results # array of Result structs
results.each do |result| # iterate over the Result structs
line_num = 0
File.open("#{result.file_path}", "r") do |f|
f.each_line do |line|
line_num = line_num + 1
if line_num < (result.line_number.to_i - 5) || line_num > (result.line_number.to_i + 15)
next
else
result.full_code << line
end
end
end
end
end
With the struct defined as such, I get to call result.file_path
rather than the uglier, non-semantic line_array[0]
. This is especially helpful when I write the .rb.erb file (yes, I wrote a .rb.erb file). Here’s what that looks like with the struct in place:
### Output from Recall search
<% @results.each do |result| %>
###########################
# File: <%= result.file_path %>
# Starting at line number: <%= result.line_number %>...
<% result.full_code.each { |line| %><%= line %><% } %>
<% end %>
3. Reading the “full_code” snippet from each file
Another fun bit was figuring out how to get the 20 lines surrounding the query in the result file. Let’s say we’re searching for .to_s
. The grep search returns just the single line that .to_s
is on. But I knew that wouldn’t be very helpful to the user– they’d want more context than that.
I figured I’d give them the 5 lines before the query, and 15 lines after. The way I did this (in results.rb) is a little janky, but it works OK.
Calling result.line_number
gives the line number that the grep search found the query on. (It returns the line number in its results thanks to the -n flag I hard-code into the call.) I also make a counter called line_num
that iterates each time we read another line in the file.
File.open("#{result.file_path}", "r") do |f|
f.each_line do |line|
line_num = line_num + 1
if line_num < (result.line_number.to_i - 5) || line_num > (result.line_number.to_i + 15)
next
else
result.full_code << line
end
end
end
I call next
if the counter is too far above or below the query’s line number.
Obviously feel free to fork and submit a pull request– plenty to improve on.