Fix up Rubinius specific library specs.
[rbx.git] / lib / debugger / output.rb
bloba3faae64bb8cc30e1f186d2a3ab9a84d8e9c7575
1 class Debugger
2   # Class used to return formatted output to the +Debugger+ for display.
3   # Command implementations should not output anything directly, but should
4   # instead return either a string, or an instance of this class if the output
5   # needs to be formatted in some way.
6   class Output
8     ##
9     # Class for defining columns of output.
10     
11     class Columns
12       FORMAT_RE = /%([|-])?(0)?(\d*|\*)([sd])/
13       
14       # Defines a new Columns block for an output.
15       # Takes the following arguments:
16       # - formats: Either a fixnum specifying the number of columns, or an array
17       #   of format specifications (one per column). If an array is passed, the
18       #   items of the array may be either a format specification as handled by
19       #   String#%, or an array containing the column header and a format 
20       #   specification.
21       # - An optional column separator string that will be inserted between
22       #   columns when converting a row of cells to a string; defaults to a
23       #   single space.
24       # Column widths will be fixed at whatever size is specified in the format,
25       # or if none is specified, to whatever the necessary width is requried to
26       # output all values in that column. The special width specification * can
27       # be used to indicate that all remaining space on the line should be set
28       # as the column width.
29       def initialize(formats, col_sep=' ')
30         if formats.kind_of? Array
31           @formats = []
32           @headers = nil
33           formats.each_with_index do |fmt,i|
34             if fmt.kind_of? Array
35               @headers = [] unless @headers
36               @headers[i] = fmt.first if fmt.size > 1
37               @formats[i] = fmt.last
38             else
39               @formats[i] = fmt
40             end
41           end
42         elsif formats.kind_of? Fixnum
43           @formats = Array.new(formats, '%-s')
44           @headers = nil
45         else
46           raise ArgumentError, "The formats arg must be an Array or a Fixnum (got #{formats.class})"
47         end
48         @col_separator = col_sep
50         @fixed_widths = Array.new(@formats.size)
51         @widths = Array.new(@formats.size, 0)
53         # Use widths specified in format string        
54         @formats.each_with_index do |fmt, i|
55           fmt =~ FORMAT_RE
56           raise ArgumentError, "Invalid format specification" unless $4
57           if $3 and $3.length > 0
58             if $3 != '*'
59               @fixed_widths[i] = $3.to_i + $`.length + $'.length
60               @widths[i] = @fixed_widths[i]
61             else
62               @fixed_widths[i] = '*'
63             end
64           end
65         end
67         # Initialise column widths to column header widths
68         if @headers
69           @headers.each_with_index do |hdr, i|
70             @widths[i] = hdr.length if hdr and hdr.length > @widths[i]
71           end          
72         end
73       end
74       
75       attr_reader :fixed_widths, :widths
76       
77       # Update the column widths required based on the row content
78       def update_widths(cells)
79         cells.each_with_index do |cell,col|
80           if cell
81             @formats[col] =~ FORMAT_RE
82             str = "#{$`}%#{$2}#{$3 unless $3 == '*'}#{$4}#{$'}" % cell
83             @widths[col] = str.length if str.length > @widths[col]
84           end
85         end
86       end
87       
88       # Redistributes the calculated widths, ensuring the overall line width is
89       # no greater than the specified page width. Reductions are made in the
90       # following order:
91       # - first, all columns that have a width specification are reduced to that
92       #   width (if they exceed it)
93       # - next, any columns with a variable width specification are reduced 
94       #   (from largest to smallest) proportionately based on the needed reduction
95       def redistribute_widths(page_width, indent=0)
96         if page_width
97           # Reduce page_wdith by any requirements for indentation and column separators
98           page_width -= indent + (@widths.size-1) * @col_separator.length
99           raise ArgumentError, "Page width is insufficient to display any content" if page_width < 1
101           # Determine the fixed and variable width columns
102           fixed_width = 0
103           var_width = 0
104           cum_width = 0
105           variable_widths = []
106           @widths.each_with_index do |width,i|
107             cum_width += @widths[i]
108             if @fixed_widths[i]
109               if fixed_widths[i] == '*'
110                 variable_widths << i
111                 var_width += @widths[i]
112               else
113                 fixed_width += @fixed_widths[i]
114               end
115             end
116           end
117           return true if cum_width <= page_width
119           # Need to squeeze - first up, ensure fixed width columns don't exceed
120           # specified size
121           @fixed_widths.each_with_index do |fw,i|
122             if fw and fw != '*'
123               if fw < @widths[i]
124                 cum_width -= @widths[i] - fw
125                 @widths[i] = fw
126               end
127             end
128           end
129           return true if cum_width <= page_width
131           if variable_widths.size > 0 and (fixed_width + variable_widths.size) < page_width
132             # Next, reduce variable widths proportionately to needs      
133             cum_adj = 0
134             variable_widths.sort.each do |i|
135               adj = (@widths[i].to_f / var_width * (cum_width - page_width)).ceil
136               @widths[i] -= adj
137               cum_adj += adj
138               break if cum_width - cum_adj <= page_width
139             end
140             cum_width -= cum_adj
141           end
142           return true if cum_width <= page_width
144           if cum_width > page_width
145             # TODO: Full squeeze - squeeze all columns to fit
146           end
148           return false
149         end
150         true
151       end
153       # Returns true if the column specification has headers
154       def has_headers?
155         !@headers.nil?
156       end
158       # Returns a count of the number of columns defined
159       def count
160         @formats.size
161       end
162       
163       # Returns a formatted string containing the column headers.
164       # Takes two optional parameters:
165       # - indent: specifies the number of characters to indent the line by
166       #   (default is 0).
167       # - page_width: specifies a page width to which the output should be made
168       #   to fit. If nil (the default), output is not forced to fit any width.
169       def format_header_str(indent=0)
170         if @headers
171           hdr = [nil]
172           hdr.concat format_str(@headers, indent-1, Array.new(@formats.size, '%|s'))
173           str = ' ' * (indent-1) + '+' if indent > 0
174           @widths.each do |width|
175             str << '-' * width + '+'
176           end
177           hdr << str
178           hdr
179         end
180       end
182       # Format an array of cells into a string
183       def format_str(row, indent=0, formats=@formats)
184         cells = []
185         formats.each_with_index do |fmt, i|
186           if row[i]
187             # Format cell ignoring width and alignment, wrapping if necessary
188             fmt =~ FORMAT_RE
189             cell = "#{$`}%#{$4}#{$'}" % row[i]
190             align = case $1
191             when '-' then :left
192             when '|' then :center
193             else :right
194             end
195             pad = $2 || ' '
196             lines = wrap(cell, @widths[i], align, pad)
197             cells << lines
198           else
199             cells << []
200           end
201         end
203         line, last_line = 0, 1
204         str = []
205         while line < last_line do
206           line_cells = []
207           cells.each_with_index do |cell, i|
208             last_line = cell.size if line == 0 and cell.size > last_line
209             if line < cell.size
210               line_cells << cell[line]
211             else
212               # Cell does not wrap onto current line, so just output spaces
213               line_cells << ' ' * @widths[i]
214             end
215           end
216           str << ' ' * indent + line_cells.join(@col_separator)
217           line += 1
218         end
219         str
220       end
222       # Splits the supplied string at logical breaks to ensure that no line is
223       # longer than the spcecified width. Returns an array of lines.
224       def wrap(str, width, align=:none, pad=' ')
225         raise ArgumentError, "Invalid wrap length specified (#{width})" if width < 0
227         return [nil] unless str and width > 0
229         str.rstrip!
230         lines = []
231         until str.length <= width do
232           if pos = str[0, width].rindex(/[\s\-,\/_]/)
233             # Found a break on whitespace or dash
234             line, str = str[0..pos].rstrip, str[pos+1..-1].strip
235           elsif pos = str[0, width-1].rindex(/[^\w]/) and pos > 0
236             # Found a non-word character to break on
237             line, str = str[0...pos].rstrip, str[pos..-1].strip
238           else
239             # Force break at width
240             line, str = str[0...width].rstrip, str[width..-1].strip
241           end
243           # Pad with spaces to requested width if an alignment is specified
244           lines << align(line, width, align, pad)
245         end
246         lines << align(str, width, align, pad) if str
247         lines
248       end
250       # Aligns 
251       def align(line, width, align, pad=' ')
252         case align
253         when :left
254           line = line + pad * (width - line.length)
255         when :right
256           line = pad * (width - line.length) + line
257         when :center
258           line = line.center(width, pad)
259         else
260           line
261         end
262       end
263     end
265     ##
266     # Class for colorizing output lines
268     class Color
269       def initialize(color=:clear)
270         @color = color
271       end
272       attr_accessor :color
274       # Set the color
275       def escape
276         case @color
277         when :blue
278           "\033[0;34m"
279         when :red
280           "\033[0;31m"
281         when :green
282           "\033[0;32m"
283         when :yellow
284           "\033[0;33m"
285         when :blue
286           "\033[0;34m"
287         when :magenta
288           "\033[0;35m"
289         when :cyan
290           "\033[0;36m"
291         when :white
292           "\033[0;37m"
293         when :clear
294           "\033[0m"
295         else
296           @color
297         end
298       end
300       # Clear the color
301       def clear
302         "\033[0m"
303       end
305       def to_s
306         @color.to_s
307       end
308     end
310     ##
311     # Class for marking a line in some way
312     
313     class LineMarker
314       def initialize(marker='=> ')
315         @marker = marker
316       end
318       def length
319         @marker.length
320       end
322       def to_s
323         @marker
324       end
325     end
327     # Convenience method for creating an Output of type :info
328     def self.info(msg, color=nil)
329       out = new(:info)
330       out.set_color color if color
331       out << msg
332       out.set_color :clear if color
333       out
334     end
336     # Convenience method for creating an Output of type :none
337     def self.none(msg, color=nil)
338       out = new(:none)
339       out.set_color color if color
340       out << msg
341       out.set_color :clear if color
342       out
343     end
345     # Convenience method for creating an Output of type :error
346     def self.error(msg, color=nil)
347       out = new(:error)
348       out.set_color color if color
349       out << msg
350       out.set_color :clear if color
351       out
352     end
354     # Initializes a new output instance.
355     # Output may be of different types, with the default being :info.
356     # The output type is not used by the Output class, but may be significant
357     # to the interface that displays the output. Commonly used output types
358     # include:
359     # - :info, the default for informational output
360     # - :none, to indicate there is no output of the requested type
361     # - :error, to indicate a command error
362     def initialize(output_type=:info)
363       @output_type = output_type
364       clear
365     end
367     # Clears all output settings
368     def clear
369       @output = []
370       @current_cols = nil
371       @current_color = nil
372       @marker_width = 0
373     end
374     attr_reader :output, :current_cols, :current_color
376     # Adds a new row of data to the output
377     # The item to be added may be an object of several different types:
378     # - An array is regarded as a series of cells that make up a row in a table.
379     # - A string is regarded as a complete row of data.
380     # - A Columns object defines the columnar aspects of subsequent rows of 
381     #   data, which are typically arrays.
382     # - A Color object defines a color change that remains in force until it is
383     #   cleared.
384     # - LineMarker object specifies a marker to appear to the left of the next
385     #   row.
386     def <<(item)
387       case item
388       when Array
389         # Line contains multiple columns of text
390         unless @current_cols and item.size == @current_cols.count
391           # Normally, a command will explicitly specify column formats via a
392           # call to set_columns; however, if the output stream receives an array
393           # of objects with a different column count to previous lines, a new
394           # Columns instance is auto-created.
395           @current_cols = Columns.new(item.size)
396           @output << @current_cols
397         end
398         @current_cols.update_widths(item)
399       when Columns
400         @current_cols = item
401       when Color
402         @current_color = item
403       when LineMarker
404         @marker_width = item.length if @marker_width < item.length
405       end
406       @output << item
407     end
409     # Convenience method to set a new column structure
410     def set_columns(formats, col_sep=' ')
411       self << Columns.new(formats, col_sep)
412     end
414     # Convenience method to set a new row color
415     def set_color(color)
416       self << Color.new(color) unless @current_color and @current_color.color == color
417     end
419     # Sets a marker to be displayed next to the next line
420     def set_line_marker(marker='=> ')
421       self << LineMarker.new(marker)
422     end
424     # Convert this output stream to a string
425     def to_s
426       lines.join("\n")
427     end
428     
429     # Return the output as an array of strings, one per line. If an output item
430     # wraps to more than one line, each line will be a seperate entry in the
431     # returned array.
432     # Takes an optional parameter +page_width+ that specifies the width of the 
433     # page on which the output will be displayed.
434     def lines(page_width=nil)
435       column = nil
436       color = nil
437       marker = nil
438       lines = []
439       @output.each do |item|
440         case item
441         when String
442           str = ""
443           str << color.escape if color
444           str << item.rstrip
445           str << color.clear if color
446           lines << str
447         when Columns
448           column = item
449           if page_width
450             # Check page_width is sufficient for marker and column headers
451             width = page_width-1 - (@marker_width + 2)
452             column.redistribute_widths(page_width-1, @marker_width + 2) if width > 0
453           end
454           if column.has_headers?
455             lines.concat column.format_header_str(@marker_width+2)
456           end
457         when Array
458           str = ""
459           str << color.escape if color
460           str << output_marker(marker)
461           marker = nil
462           l = column.format_str(item, @marker_width+2)
463           l.first[0, @marker_width] = ''
464           l.first.insert 0, str
465           l.last << color.clear if color
466           lines.concat l
467         when Color
468           if item.color == :clear
469             color = nil
470           else
471             color = item
472           end
473         when LineMarker
474           marker = item
475         end
476       end
477       lines
478     end
480     def output_marker(marker)
481       str = ''
482       if @marker_width > 0
483         if marker
484           str = marker.to_s
485         else
486           str = ' ' * @marker_width
487         end
488       end
489       str
490     end
491   end