]> git.cworth.org Git - sup/blob - lib/sup/modes/scroll-mode.rb
Merge branch 'sent-poll-fix' into next
[sup] / lib / sup / modes / scroll-mode.rb
1 module Redwood
2
3 class ScrollMode < Mode
4   ## we define topline and botline as the top and bottom lines of any
5   ## content in the currentview.
6   
7   ## we left leftcol and rightcol as the left and right columns of any
8   ## content in the current view. but since we're operating in a
9   ## line-centric fashion, rightcol is always leftcol + the buffer
10   ## width. (whereas botline is topline + at most the buffer height,
11   ## and can be == to topline in the case that there's no content.)
12
13   attr_reader :status, :topline, :botline, :leftcol
14
15   COL_JUMP = 2
16
17   register_keymap do |k|
18     k.add :line_down, "Down one line", :down, 'j', 'J'
19     k.add :line_up, "Up one line", :up, 'k', 'K'
20     k.add :col_left, "Left one column", :left, 'h'
21     k.add :col_right, "Right one column", :right, 'l'
22     k.add :page_down, "Down one page", :page_down, ' '
23     k.add :page_up, "Up one page", :page_up, 'p', :backspace
24     k.add :jump_to_start, "Jump to top", :home, '^', '1'
25     k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
26     k.add :jump_to_left, "Jump to the left", '['
27     k.add :search_in_buffer, "Search in current buffer", '/'
28     k.add :continue_search_in_buffer, "Jump to next search occurrence in buffer", BufferManager::CONTINUE_IN_BUFFER_SEARCH_KEY
29   end
30
31   def initialize opts={}
32     @topline, @botline, @leftcol = 0, 0, 0
33     @slip_rows = opts[:slip_rows] || 0 # when we pgup/pgdown,
34                                        # how many lines do we keep?
35     @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
36     @search_query = nil
37     @search_line = nil
38     @status = ""
39     super()
40   end
41
42   def rightcol; @leftcol + buffer.content_width; end
43
44   def draw
45     ensure_mode_validity
46     (@topline ... @botline).each { |ln| draw_line ln }
47     ((@botline - @topline) ... buffer.content_height).each do |ln|
48       if @twiddles
49         buffer.write ln, 0, "~", :color => :twiddle_color
50       else
51         buffer.write ln, 0, ""
52       end
53     end
54     @status = "lines #{@topline + 1}:#{@botline}/#{lines}"
55   end
56
57   def in_search?; @search_line end
58   def cancel_search!; @search_line = nil end
59
60   def continue_search_in_buffer
61     unless @search_query
62       BufferManager.flash "No current search!"
63       return
64     end
65
66     start = @search_line || search_start_line
67     line, col = find_text @search_query, start
68     if line.nil? && (start > 0)
69       line, col = find_text @search_query, 0
70       BufferManager.flash "Search wrapped to top!" if line
71     end
72     if line
73       @search_line = line + 1
74       search_goto_pos line, col, col + @search_query.length
75       buffer.mark_dirty
76     else
77       BufferManager.flash "Not found!"
78     end
79   end
80
81   def search_in_buffer
82     query = BufferManager.ask :search, "search in buffer: "
83     return if query.nil? || query.empty?
84     @search_query = Regexp.escape query
85     continue_search_in_buffer
86   end
87
88   ## subclasses can override these two!
89   def search_goto_pos line, leftcol, rightcol
90     jump_to_line line
91
92     if rightcol > self.rightcol # if it's occluded...
93       jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
94     end
95   end
96   def search_start_line; @topline end
97
98   def col_left
99     return unless @leftcol > 0
100     @leftcol -= COL_JUMP
101     buffer.mark_dirty
102   end
103
104   def col_right
105     @leftcol += COL_JUMP
106     buffer.mark_dirty
107   end
108
109   def jump_to_col col
110     col = col - (col % COL_JUMP)
111     buffer.mark_dirty unless @leftcol == col
112     @leftcol = col
113   end
114
115   def jump_to_left; jump_to_col 0; end
116
117   ## set top line to l
118   def jump_to_line l
119     l = l.clamp 0, lines - 1
120     return if @topline == l
121     @topline = l
122     @botline = [l + buffer.content_height, lines].min
123     buffer.mark_dirty
124   end
125
126   def at_top?; @topline == 0 end
127   def at_bottom?; @botline == lines end
128
129   def line_down; jump_to_line @topline + 1; end
130   def line_up;  jump_to_line @topline - 1; end
131   def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
132   def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
133   def jump_to_start; jump_to_line 0; end
134   def jump_to_end; jump_to_line lines - buffer.content_height; end
135
136   def ensure_mode_validity
137     @topline = @topline.clamp 0, [lines - 1, 0].max
138     @botline = [@topline + buffer.content_height, lines].min
139   end
140
141   def resize *a
142     super(*a)
143     ensure_mode_validity
144   end
145
146 protected
147
148   def find_text query, start_line
149     regex = /#{query}/i
150     (start_line ... lines).each do |i|
151       case(s = self[i])
152       when String
153         match = s =~ regex
154         return [i, match] if match
155       when Array
156         offset = 0
157         s.each do |color, string|
158           match = string =~ regex
159           if match
160             return [i, offset + match]
161           else
162             offset += string.length
163           end
164         end
165       end
166     end
167     nil
168   end
169
170   def draw_line ln, opts={}
171     regex = /(#{@search_query})/i
172     case(s = self[ln])
173     when String
174       if in_search?
175         draw_line_from_array ln, matching_text_array(s, regex), opts
176       else
177         draw_line_from_string ln, s, opts
178       end
179     when Array
180       if in_search?
181         ## seems like there ought to be a better way of doing this
182         array = []
183         s.each do |color, text| 
184           if text =~ regex
185             array += matching_text_array text, regex, color
186           else
187             array << [color, text]
188           end
189         end
190         draw_line_from_array ln, array, opts
191       else
192         draw_line_from_array ln, s, opts
193       end
194     else
195       raise "unknown drawable object: #{s.inspect} in #{self} for line #{ln}" # good for debugging
196     end
197
198       ## speed test
199       # str = s.map { |color, text| text }.join
200       # buffer.write ln - @topline, 0, str, :color => :none, :highlight => opts[:highlight]
201       # return
202   end
203
204   def matching_text_array s, regex, oldcolor=:none
205     s.split(regex).map do |text|
206       next if text.empty?
207       if text =~ regex
208         [:search_highlight_color, text]
209       else
210         [oldcolor, text]
211       end
212     end.compact + [[oldcolor, ""]]
213   end
214
215   def draw_line_from_array ln, a, opts
216     xpos = 0
217     a.each do |color, text|
218       raise "nil text for color '#{color}'" if text.nil? # good for debugging
219       
220       if xpos + text.length < @leftcol
221         buffer.write ln - @topline, 0, "", :color => color,
222                      :highlight => opts[:highlight]
223         xpos += text.length
224       elsif xpos < @leftcol
225         ## partial
226         buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
227                      :color => color,
228                      :highlight => opts[:highlight]
229         xpos += text.length
230       else
231         buffer.write ln - @topline, xpos - @leftcol, text,
232                      :color => color, :highlight => opts[:highlight]
233         xpos += text.length
234       end
235     end
236   end
237
238   def draw_line_from_string ln, s, opts
239     buffer.write ln - @topline, 0, s[@leftcol .. -1], :highlight => opts[:highlight]
240   end
241 end
242
243 end
244