Blob of Support/lib/grep_helpers.rb (raw blob data)

1 require 'cgi'
2 require 'rubygems'
3 gem 'rb-appscript', '>=0.5.1'
4 require 'appscript'
5 require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/ruby_tm_helpers.rb"
6
7 # Output-code derived from Henrik Nyh <http://henrik.nyh.se> 2007-06-26
8
9 %w{ui web_preview escape}.each { |lib| require "%s/lib/%s" % [ENV['TM_SUPPORT_PATH'], lib] }
10
11 module GrepperDisplayHelpers
12 def ellipsize_path(path)
13 path.sub(/^(.{30})(.{10,})(.{30})$/) { "#$1⋯#$3" }
14 end
15
16 def short_path(path, paths_to_display = 2)
17 path.split("/")[(-1 - paths_to_display)..(-1)] * "/"
18 end
19 end
20
21 class Grepper
22 include GrepperDisplayHelpers
23
24 attr_accessor :name, :exclude_files
25 attr_reader :query, :include_files
26 attr_accessor :fixed_strings
27 attr_writer :query_highlight_regexp
28 attr_accessor :case_sensitive
29
30 def query_highlight_regexp
31 @query_highlight_regexp || query_regexp
32 end
33
34 def query=(regexp)
35 @query_regexp = nil
36 regexp = regexp.inspect if regexp.is_a?(Regexp)
37 if /^\/(.+)\/([i]*)$/.match(regexp)
38 @query = $1
39 self.case_sensitive = $2 != "i"
40 # puts "<div>$2 = #{$2} #{self.case_sensitive}</div>"
41 self.fixed_strings = false
42 else
43 self.fixed_strings = true
44 @query = regexp
45 self.case_sensitive = true
46 end
47 end
48
49 def query_regexp
50 @query_regexp ||= begin
51 o =
52 if fixed_strings
53 Regexp.escape(query).gsub("/", '\/')
54 else
55 query
56 end
57 Regexp.new(o, case_sensitive ? nil : Regexp::IGNORECASE)
58 end
59 end
60
61 attr_writer :title
62
63 def title
64 @title || %!Searching for #{ query_regexp.inspect }!
65 end
66
67
68 def parse_regexp(regexp)
69 self.query = regexp
70 # puts command
71 end
72
73 def initialize(name, options = {})
74 self.name = name
75 self.fixed_strings = options[:fixed_strings]
76 self.query = options[:query]
77 self.exclude_files = [options[:exclude_files] || %w[*.log *.log*]].compact.flatten
78 @include_files = [options[:include_files]].compact.flatten || []
79 self.case_sensitive = true
80 end
81
82 def bail(message)
83 puts <<-HTML
84 <h2>#{ message }</h2>
85 HTML
86 puts html_footer
87 exit
88 end
89
90 def directory
91 ENV['TM_PROJECT_DIRECTORY'] || ( ENV['TM_FILEPATH'] && File.dirname(ENV['TM_FILEPATH']) )
92 end
93
94 def abort
95 puts "Search aborted"
96 exit_show_tool_tip
97 end
98
99 def matches
100 return @matches if @matches
101
102 @matches = []
103 IO.popen(command) do |pipe|
104 last_path = path = i = nil
105 pipe.each_with_index do |line, i|
106 if line =~ /^(Binary file )(.*?) matches/
107 prefix, file = $1, $2
108 path = directory + file[1..-1]
109 @matches << {
110 :binary_file => true,
111 :prefix => prefix,
112 :file => file,
113 :path => path
114 }
115 next
116 end
117
118 line.gsub!(/^([^:]+):(\d+):(.*)$/) do
119 relative_path, line_number, content = $1, $2, $3.strip
120 path = directory + relative_path[1..-1]
121 @matches << {
122 :relative_path => relative_path,
123 :line_number => line_number,
124 :content => content,
125 :path => path
126 }
127 end
128 last_path = path
129 end
130 end
131 @matches
132 end
133
134 def tm_goto_match(m)
135 tm_open(m[:path], :line => m[:line_number])
136 end
137
138 def run(&block)
139 bail("Not in a saved file") unless directory
140
141 yield
142
143 abort unless query
144
145 if matches.length == 1
146 tm_goto_match(matches.first)
147 exit_discard
148 end
149
150 if matches.length == 0
151 puts "No results - #{title}"
152 exit_show_tool_tip
153 end
154 # matches
155
156 display
157 end
158
159 def command
160 include_param = include_files.map{|f| "--include='#{f}'"} * " "
161 fixed_strings_param = fixed_strings ? "--fixed-strings" : "-E"
162 case_sensitive_param = case_sensitive ? "" : "--ignore-case"
163 exclude_param = exclude_files.map{|f| "--exclude='#{f}'"} * " "
164 find_command = Finder.new(".").command
165 command =
166 %{cd "#{directory}"; #{find_command} | xargs -0 grep -nr #{case_sensitive_param} #{fixed_strings_param} #{exclude_param} #{include_param} #{e_sh query}}
167 end
168
169 def display
170 puts "implement me!"
171 end
172 end
173
174 class Finder
175 attr_accessor :path
176
177 def initialize(path=".")
178 self.path = path
179 end
180
181 def command
182 "find #{path} \\( -path '*/.svn' -or -path '*/vendor/rails' \\) -prune -or -type f -print0"
183 end
184
185 def results
186 @results = []
187
188 IO.popen(command) do |pipe|
189 pipe.read.split("\000").each do |line|
190 @results << line
191 end
192 end
193 @results
194 end
195 end
196
197 class GrepperMenu < Grepper
198 def display
199 match_index = TextMate::UI.menu(matches.map{|m| "#{short_path(m[:path])}:#{m[:line_number]} - #{m[:content]}"})
200 exit_discard if match_index.nil?
201
202 tm_goto_match(matches[match_index])
203 exit_show_tool_tip
204 end
205 end
206
207 class GrepperHTML < Grepper
208
209 def escape(string)
210 CGI.escapeHTML(string)
211 end
212
213 # sadly... this causes textmate to crash. Can't do this.
214 def html_close_tm_window
215 <<-EOF
216 <script type="text/javascript">
217 setTimeout(function() {
218 TextMate.close();
219 }, 1000);
220 </script>
221 EOF
222 end
223
224 def display
225
226 puts html_for_head
227 puts html_for_body
228 puts <<-HTML
229 <h2>#{escape title}</h2>
230 <table>
231 HTML
232
233 last_path = path = i = nil
234 matches.each_with_index do |match, i|
235
236 if match[:binary_file]
237 puts <<-HTML
238 <tr class="binary #{ 'odd' unless i%2==0 }">
239 <td>
240 #{ prefix }
241 <a href="javascript:reveal_file('#{ escape(match[:path]) }')" title="#{ escape(match[:path]) }">#{ ellipsize_path(match[:file]) }</a>
242 </td>
243 <td></td>
244 </tr>
245 #{ html_for_resize_table if i%100==0 }
246 HTML
247 next
248 end
249
250 url = "txmt://open/?url=file://#{match[:path]}&line=#{match[:line_number]}"
251 highlighted_content = escape(match[:content]).
252 # Highlight keywords
253 gsub(query_highlight_regexp) { %{<strong class="keyword">#$&</strong>} }.
254 # Ellipsize before, between and after keywords
255 gsub(%r{(^[^<]{25}|</strong>[^<]{15})([^<]{20,})([^<]{15}<strong|[^<]{25}$)}) do
256 %{#$1<span class="ellipsis" title="#{escape($2)}">⋯</span>#$3}
257 end
258 puts <<-HTML
259 <tr class="#{ 'odd' unless i%2==0 } #{ 'newFile' if (match[:path] != last_path) }">
260 <td>
261 <a href="#{ url }" title="#{ "%s:%s" % [match[:path], match[:line_number]] }">
262 #{ "%s:%s" % [ellipsize_path(match[:relative_path]), match[:line_number]] }
263 </a>
264 </td>
265 <td>#{ highlighted_content }</td>
266 </tr>
267 HTML
268 last_path = path
269 end
270 if i
271 # A paragraph inside the table ends up at the top even though it's output
272 # at the end. Something of a hack :)
273 i += 1
274 puts <<-HTML
275 <p>#{i} matching line#{i==1 ? '' : 's'}:</p>
276 #{html_for_resize_table}
277 HTML
278 else
279 puts <<-HTML
280 <tr id="empty"><td colspan="2">No results.</td></tr>
281 HTML
282 end
283
284 puts <<-HTML
285 </table>
286 HTML
287
288 html_footer
289
290 puts html_close_tm_window
291
292 exit_show_html
293 end
294
295 def html_for_head
296 html_head(
297 :window_title => name,
298 :page_title => name,
299 :sub_title => directory || "Error",
300 :html_head => name
301 )
302 end
303
304 def html_for_resize_table
305 <<-HTML
306 <script type="text/javascript">
307 resizeTableToFit();
308 </script>
309 HTML
310 end
311
312 def html_for_body
313 <<-HTML
314 <style type="text/css">
315 table { font-size:0.9em; border-collapse:collapse; border-bottom:1px solid #555; }
316 h2 { font-size:1.3em; }
317 tr { background:#FFF; }
318 tr.odd { background:#EEE; }
319 td { vertical-align:top; white-space:nowrap; padding:0.4em 1em; color:#000 !important; }
320 tr td:first-child { text-align:right; padding-right:1.5em; }
321 td a { color:#00F !important; }
322 tr.binary { background:#E8AFA8; }
323 tr.binary.odd { background:#E0A7A2; }
324 tr#empty { border-bottom:1px solid #FFF; }
325 tr#empty td { text-align:center; }
326 tr.newFile, tr.binary { border-top:1px solid #555; }
327 .keyword { font-weight:bold; background:#F6D73A; margin:0 0.1em; }
328 .ellipsis { color:#777; margin:0 0.5em; }
329 </style>
330 <script type="text/javascript">
331 function reveal_file(path) {
332 const quote = '"';
333 const command = "osascript -e ' tell app "+quote+"Finder"+quote+"' " +
334 " -e 'reveal (POSIX file " +quote+path+quote + ")' " +
335 " -e 'activate' " +
336 " -e 'end' ";
337 TextMate.system(command, null);
338 }
339
340 function findPos(obj) {
341 var curleft = curtop = 0;
342 if (obj.offsetParent) {
343 curleft = obj.offsetLeft
344 curtop = obj.offsetTop
345 while (obj = obj.offsetParent) {
346 curleft += obj.offsetLeft
347 curtop += obj.offsetTop
348 }
349 }
350 return {left: curleft, top: curtop};
351 }
352
353 function resizeTableToFit() {
354 var table = document.getElementsByTagName("table")[0];
355 const minWidth = 450, minHeight = 250;
356
357 var pos = findPos(table);
358 var tableFitWidth = table.offsetWidth + pos.left * 2;
359 var tableFitHeight = table.offsetHeight + pos.top + 50;
360 var screenFitWidth = screen.width - 150;
361 var screenFitHeight = screen.height - 150;
362
363 var setWidth = tableFitWidth > screenFitWidth ? screenFitWidth : tableFitWidth;
364 var setHeight = tableFitHeight > screenFitHeight ? screenFitHeight : tableFitHeight;
365 setWidth = setWidth < minWidth ? minWidth : setWidth;
366 setHeight = setHeight < minHeight ? minHeight : setHeight;
367
368 window.resizeTo(setWidth, setHeight);
369 }
370
371 </script>
372 HTML
373 end
374
375 end