tp_tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,687 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'curses'
4
+ require 'set'
5
+ require_relative 'xml_formatter'
6
+ require_relative 'tree_node'
7
+
8
+ module TPTree
9
+ class InteractiveViewer
10
+ include XMLFormatter
11
+
12
+ def initialize(tree)
13
+ @tree = tree
14
+ @original_tree = tree # Keep reference to original tree for navigation
15
+ @tree_stack = [] # Stack to track zoom levels
16
+ @lines = []
17
+ @visible_lines = []
18
+ @scroll_pos = 0
19
+ @cursor_pos = 0
20
+ @stdscr = Curses.init_screen
21
+ @expanded_nodes = Set.new
22
+ @node_children = {}
23
+
24
+ # Initialize all nodes as expanded and build parent-child relationships
25
+ analyze_tree_structure
26
+ expand_all_initially
27
+ end
28
+
29
+ def show
30
+ Curses.start_color
31
+ init_color_pairs
32
+ Curses.curs_set(0) # Hide cursor
33
+ Curses.noecho
34
+ @stdscr.keypad(true)
35
+
36
+ prepare_lines
37
+ update_visible_lines
38
+ main_loop
39
+ ensure
40
+ Curses.close_screen
41
+ end
42
+
43
+ private
44
+
45
+ def analyze_tree_structure
46
+ @node_children = {}
47
+ call_stack = []
48
+
49
+ @tree.each_with_index do |node, index|
50
+ case node.event
51
+ when :call
52
+ # This is a call that will have children (has separate return)
53
+ call_stack.push(index)
54
+ @node_children[index] = []
55
+ when :return
56
+ # This is a return, pop the corresponding call
57
+ if call_stack.any?
58
+ call_index = call_stack.pop
59
+ # Mark all nodes between call and return as children
60
+ @node_children[call_index] = ((call_index + 1)...index).to_a
61
+ end
62
+ when :call_return
63
+ # This is a leaf node (no children)
64
+ @node_children[index] = []
65
+ end
66
+ end
67
+ end
68
+
69
+ def expand_all_initially
70
+ @node_children.each_key do |index|
71
+ @expanded_nodes.add(index) if has_children?(index)
72
+ end
73
+ end
74
+
75
+ def has_children?(index)
76
+ @node_children[index] && @node_children[index].any?
77
+ end
78
+
79
+ def is_expanded?(index)
80
+ @expanded_nodes.include?(index)
81
+ end
82
+
83
+ def toggle_expansion(index)
84
+ if has_children?(index)
85
+ if is_expanded?(index)
86
+ @expanded_nodes.delete(index)
87
+ else
88
+ @expanded_nodes.add(index)
89
+ end
90
+ update_visible_lines
91
+ end
92
+ end
93
+
94
+ def update_visible_lines
95
+ @visible_lines = []
96
+ @tree.each_with_index do |node, index|
97
+ @visible_lines << index if should_show_node?(index)
98
+ end
99
+ end
100
+
101
+ def should_show_node?(index)
102
+ # Always show root level nodes
103
+ return true if @tree[index].depth == 0
104
+
105
+ # Check if ALL ancestors are expanded
106
+ all_ancestors_expanded?(index)
107
+ end
108
+
109
+ def all_ancestors_expanded?(index)
110
+ current_depth = @tree[index].depth
111
+
112
+ # Check each ancestor level
113
+ (1..current_depth).each do |depth_to_check|
114
+ ancestor_index = find_ancestor_at_depth(index, depth_to_check - 1)
115
+ next if ancestor_index.nil?
116
+
117
+ # If this ancestor has children and is not expanded, hide this node
118
+ if has_children?(ancestor_index) && !is_expanded?(ancestor_index)
119
+ return false
120
+ end
121
+ end
122
+
123
+ true
124
+ end
125
+
126
+ def find_ancestor_at_depth(index, target_depth)
127
+ current_depth = @tree[index].depth
128
+
129
+ # Look backwards for a call at the target depth
130
+ (index - 1).downto(0) do |i|
131
+ node = @tree[i]
132
+ if node.depth == target_depth && (node.event == :call || node.event == :call_return)
133
+ return i
134
+ end
135
+ # Stop if we've gone too far back (past a shallower depth)
136
+ break if node.depth < target_depth
137
+ end
138
+ nil
139
+ end
140
+
141
+ def find_parent_call(index)
142
+ current_depth = @tree[index].depth
143
+
144
+ # Look backwards for a call at depth-1
145
+ (index - 1).downto(0) do |i|
146
+ node = @tree[i]
147
+ if node.depth == current_depth - 1 && (node.event == :call || node.event == :call_return)
148
+ return i
149
+ end
150
+ end
151
+ nil
152
+ end
153
+
154
+ def init_color_pairs
155
+ Curses.init_pair(Curses::COLOR_RED, Curses::COLOR_RED, Curses::COLOR_BLACK)
156
+ Curses.init_pair(Curses::COLOR_GREEN, Curses::COLOR_GREEN, Curses::COLOR_BLACK)
157
+ Curses.init_pair(Curses::COLOR_YELLOW, Curses::COLOR_YELLOW, Curses::COLOR_BLACK)
158
+ Curses.init_pair(Curses::COLOR_BLUE, Curses::COLOR_BLUE, Curses::COLOR_BLACK)
159
+ Curses.init_pair(Curses::COLOR_MAGENTA, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK)
160
+ Curses.init_pair(Curses::COLOR_CYAN, Curses::COLOR_CYAN, Curses::COLOR_BLACK)
161
+ Curses.init_pair(Curses::COLOR_WHITE, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
162
+
163
+ # Status bar color pairs with default background
164
+ Curses.init_pair(8, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
165
+ Curses.init_pair(9, Curses::COLOR_GREEN, Curses::COLOR_BLACK) # class name: green
166
+ Curses.init_pair(10, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # method name: yellow
167
+ end
168
+
169
+ def prepare_lines
170
+ @lines = @tree.map do |node|
171
+ node.to_parts(formatter: self)
172
+ end
173
+ end
174
+
175
+ def main_loop
176
+ loop do
177
+ adjust_scroll
178
+ draw
179
+ handle_input
180
+ end
181
+ end
182
+
183
+ def adjust_scroll
184
+ max_visible_lines = @stdscr.maxy - 1 # Reserve one line for status bar
185
+
186
+ if @cursor_pos < @scroll_pos
187
+ @scroll_pos = @cursor_pos
188
+ elsif @cursor_pos >= @scroll_pos + max_visible_lines
189
+ @scroll_pos = @cursor_pos - max_visible_lines + 1
190
+ end
191
+
192
+ # Ensure we don't scroll past the end
193
+ max_scroll = [@visible_lines.size - max_visible_lines, 0].max
194
+ @scroll_pos = [@scroll_pos, max_scroll].min
195
+
196
+ # Special case: if we're at the last selectable line, make sure final returns are visible
197
+ last_selectable_idx = find_last_selectable_line
198
+ if @cursor_pos == last_selectable_idx
199
+ # Calculate scroll to show the cursor and as many final visible lines as possible
200
+ desired_scroll = [@visible_lines.size - max_visible_lines, 0].max
201
+ @scroll_pos = [desired_scroll, @scroll_pos].max
202
+ end
203
+ end
204
+
205
+ def find_last_selectable_line
206
+ (@visible_lines.size - 1).downto(0) do |i|
207
+ actual_index = @visible_lines[i]
208
+ return i if @tree[actual_index].event != :return
209
+ end
210
+ 0 # fallback
211
+ end
212
+
213
+ def draw
214
+ @stdscr.clear
215
+ draw_content
216
+ draw_status_bar
217
+ @stdscr.refresh
218
+ end
219
+
220
+ def draw_content
221
+ (@stdscr.maxy - 1).times do |i|
222
+ visible_line_idx = @scroll_pos + i
223
+ next unless visible_line_idx < @visible_lines.size
224
+
225
+ actual_line_idx = @visible_lines[visible_line_idx]
226
+ display_line(@lines[actual_line_idx], i, visible_line_idx, actual_line_idx)
227
+ end
228
+ end
229
+
230
+ def display_line(parts, row, visible_line_idx, actual_line_idx)
231
+ prefix_parts, content_xml = parts
232
+ x = 0
233
+
234
+ # Add expansion indicator for nodes with children
235
+ if has_children?(actual_line_idx)
236
+ expansion_indicator = is_expanded?(actual_line_idx) ? "[-] " : "[+] "
237
+ @stdscr.attron(Curses.color_pair(Curses::COLOR_CYAN))
238
+ @stdscr.setpos(row, x)
239
+ @stdscr.addstr(expansion_indicator)
240
+ @stdscr.attroff(Curses.color_pair(Curses::COLOR_CYAN))
241
+ x += expansion_indicator.length
242
+ else
243
+ # Add spacing to align with expandable nodes
244
+ @stdscr.setpos(row, x)
245
+ @stdscr.addstr(" ")
246
+ x += 4
247
+ end
248
+
249
+ # Draw prefix (no highlighting)
250
+ prefix_parts.each do |text, color_name|
251
+ attrs = color_pair_for(color_name) || Curses.color_pair(Curses::COLOR_WHITE)
252
+
253
+ @stdscr.attron(attrs)
254
+ @stdscr.setpos(row, x)
255
+ @stdscr.addstr(text)
256
+ @stdscr.attroff(attrs)
257
+ x += text.length
258
+ end
259
+
260
+ # Draw content with method name highlighting for current line
261
+ draw_xml_string(content_xml, row, x, visible_line_idx == @cursor_pos)
262
+ end
263
+
264
+ def draw_xml_string(xml_str, row, start_x, highlight_method_name = false)
265
+ x = start_x
266
+ xml_str.scan(/(<(\w+)>)?([^<]+)(<\/\w+>)?/).each do |_, color_tag, text, _|
267
+ color_name = color_tag&.to_sym
268
+
269
+ # Highlight method name (colored text) on current line
270
+ is_method_name = highlight_method_name && color_name && color_name != :white
271
+
272
+ attrs = color_pair_for(color_name) || Curses.color_pair(Curses::COLOR_WHITE)
273
+ attrs |= Curses::A_STANDOUT if is_method_name
274
+
275
+ @stdscr.attron(attrs)
276
+ @stdscr.setpos(row, x)
277
+ @stdscr.addstr(text)
278
+ @stdscr.attroff(attrs)
279
+ x += text.length
280
+ end
281
+ end
282
+
283
+ def color_pair_for(color_name)
284
+ case color_name
285
+ when :red then Curses.color_pair(Curses::COLOR_RED)
286
+ when :green then Curses.color_pair(Curses::COLOR_GREEN)
287
+ when :yellow then Curses.color_pair(Curses::COLOR_YELLOW)
288
+ when :blue then Curses.color_pair(Curses::COLOR_BLUE)
289
+ when :magenta then Curses.color_pair(Curses::COLOR_MAGENTA)
290
+ when :cyan then Curses.color_pair(Curses::COLOR_CYAN)
291
+ else Curses.color_pair(Curses::COLOR_WHITE)
292
+ end
293
+ end
294
+
295
+ def draw_status_bar
296
+ actual_index = @visible_lines[@cursor_pos] if @cursor_pos < @visible_lines.size
297
+ return unless actual_index
298
+
299
+ node = @tree[actual_index]
300
+
301
+ # Build method signature
302
+ method_signature = if node.defined_class
303
+ class_name, separator = format_class_and_separator(node.defined_class)
304
+ "#{class_name}#{separator}#{node.method_name}"
305
+ else
306
+ node.method_name.to_s
307
+ end
308
+
309
+ # Add zoom level indicator
310
+ if @tree_stack.any?
311
+ zoom_indicator = " [#{@tree_stack.size + 1}]"
312
+ method_signature += zoom_indicator
313
+ end
314
+
315
+ # Build location info
316
+ location_info = if node.path && node.lineno
317
+ filename = File.basename(node.path)
318
+ "#{filename}:#{node.lineno}"
319
+ else
320
+ ""
321
+ end
322
+
323
+ # Create a status bar with method on left, file on right
324
+ status_bar_width = @stdscr.maxx
325
+ padding = 2
326
+
327
+ # Calculate available space for content
328
+ left_padding = " " * padding
329
+ right_padding = " " * padding
330
+
331
+ # Calculate middle spacing
332
+ used_space = left_padding.length + method_signature.length + location_info.length + right_padding.length
333
+ available_space = status_bar_width - used_space
334
+
335
+ # Handle overflow by truncating the method signature
336
+ if available_space < 0
337
+ max_method_length = status_bar_width - location_info.length - (padding * 2) - 3 # 3 for "..."
338
+ if max_method_length > 0
339
+ method_signature = method_signature[0, max_method_length] + "..."
340
+ available_space = status_bar_width - left_padding.length - method_signature.length - location_info.length - right_padding.length
341
+ else
342
+ # Extreme case: just show truncated method, no file info
343
+ method_signature = method_signature[0, status_bar_width - (padding * 2) - 3] + "..."
344
+ location_info = ""
345
+ available_space = 0
346
+ end
347
+ end
348
+
349
+ # Calculate middle spacing
350
+ middle_spacing = available_space > 0 ? " " * available_space : ""
351
+
352
+ # Start drawing from left
353
+ x = 0
354
+ white_pair = Curses.color_pair(8)
355
+
356
+ # Draw left padding in white
357
+ @stdscr.attron(white_pair)
358
+ @stdscr.setpos(@stdscr.maxy - 1, x)
359
+ @stdscr.addstr(left_padding)
360
+ x += left_padding.length
361
+ @stdscr.attroff(white_pair)
362
+
363
+ # Split method_signature into class/sep/method if separator present
364
+ class_part, sep, method_part = method_signature.rpartition(/[.#]/)
365
+ if sep.empty?
366
+ # Fallback: no separator found
367
+ class_part = ""
368
+ sep = ""
369
+ method_part = method_signature
370
+ end
371
+
372
+ # Class name in green
373
+ green_pair = Curses.color_pair(9)
374
+ unless class_part.empty?
375
+ @stdscr.attron(green_pair)
376
+ @stdscr.setpos(@stdscr.maxy - 1, x)
377
+ @stdscr.addstr(class_part)
378
+ @stdscr.attroff(green_pair)
379
+ x += class_part.length
380
+ end
381
+
382
+ # Separator in white
383
+ unless sep.empty?
384
+ @stdscr.attron(white_pair)
385
+ @stdscr.setpos(@stdscr.maxy - 1, x)
386
+ @stdscr.addstr(sep)
387
+ @stdscr.attroff(white_pair)
388
+ x += sep.length
389
+ end
390
+
391
+ # Method name in yellow
392
+ yellow_pair = Curses.color_pair(10)
393
+ @stdscr.attron(yellow_pair)
394
+ @stdscr.setpos(@stdscr.maxy - 1, x)
395
+ @stdscr.addstr(method_part)
396
+ @stdscr.attroff(yellow_pair)
397
+ x += method_part.length
398
+
399
+ # Middle spacing in white
400
+ @stdscr.attron(white_pair)
401
+ @stdscr.setpos(@stdscr.maxy - 1, x)
402
+ @stdscr.addstr(middle_spacing)
403
+ @stdscr.attroff(white_pair)
404
+ x += middle_spacing.length
405
+
406
+ # Location info in white
407
+ @stdscr.attron(white_pair)
408
+ @stdscr.setpos(@stdscr.maxy - 1, x)
409
+ @stdscr.addstr(location_info)
410
+ @stdscr.attroff(white_pair)
411
+ x += location_info.length
412
+
413
+ # Right padding in white
414
+ @stdscr.attron(white_pair)
415
+ @stdscr.setpos(@stdscr.maxy - 1, x)
416
+ @stdscr.addstr(right_padding)
417
+ @stdscr.attroff(white_pair)
418
+ end
419
+
420
+ def format_class_and_separator(klass)
421
+ class_str = klass.to_s
422
+
423
+ # Handle singleton classes like #<Class:Gem::Specification>
424
+ if class_str.match(/^#<Class:(.+)>$/)
425
+ ["#{$1}", "."] # Class method: Gem::Specification.method_name
426
+ # Handle singleton classes for instances like #<Class:#<Gem::ConfigFile:0x...>>
427
+ elsif class_str.match(/^#<Class:#<(.+?):/)
428
+ ["#{$1}.instance.class", "#"] # Instance singleton: Class.instance.class#method
429
+ # Handle regular classes and modules
430
+ else
431
+ [class_str, "#"] # Instance method: ClassName#method_name
432
+ end
433
+ end
434
+
435
+ def current_method_name
436
+ return 'N/A' if @visible_lines.empty?
437
+
438
+ actual_index = @visible_lines[@cursor_pos]
439
+ return 'N/A' unless actual_index
440
+
441
+ node = @tree[actual_index]
442
+ node.method_name.to_s
443
+ end
444
+
445
+ def handle_input
446
+ case @stdscr.getch
447
+ when Curses::KEY_UP, 'k'
448
+ move_up
449
+ when Curses::KEY_DOWN, 'j'
450
+ move_down
451
+ when Curses::KEY_RIGHT, 'l'
452
+ expand_current_node
453
+ when Curses::KEY_LEFT, 'h'
454
+ collapse_current_node
455
+ when Curses::KEY_ENTER, 10, 13 # Enter key
456
+ enter_current_call
457
+ when 'T'
458
+ collapse_all
459
+ when 't'
460
+ expand_all
461
+ when 'b' # Back - go up one level in the call stack
462
+ go_back
463
+ when 'q'
464
+ exit
465
+ end
466
+ end
467
+
468
+ def expand_current_node
469
+ return if @visible_lines.empty?
470
+
471
+ actual_index = @visible_lines[@cursor_pos]
472
+ if has_children?(actual_index) && !is_expanded?(actual_index)
473
+ @expanded_nodes.add(actual_index)
474
+ update_visible_lines
475
+ end
476
+ end
477
+
478
+ def collapse_current_node
479
+ return if @visible_lines.empty?
480
+
481
+ actual_index = @visible_lines[@cursor_pos]
482
+ if has_children?(actual_index) && is_expanded?(actual_index)
483
+ @expanded_nodes.delete(actual_index)
484
+ update_visible_lines
485
+
486
+ # If we collapsed and cursor is beyond visible lines, adjust
487
+ if @cursor_pos >= @visible_lines.size
488
+ @cursor_pos = [@visible_lines.size - 1, 0].max
489
+ end
490
+ end
491
+ end
492
+
493
+ def enter_current_call
494
+ return if @visible_lines.empty?
495
+
496
+ actual_index = @visible_lines[@cursor_pos]
497
+ current_node = @tree[actual_index]
498
+
499
+ # Only allow entering calls that have children
500
+ return unless has_children?(actual_index)
501
+
502
+ # Save current state to stack (including computed state to avoid recomputation)
503
+ @tree_stack.push({
504
+ tree: @tree,
505
+ expanded_nodes: @expanded_nodes.dup,
506
+ cursor_pos: @cursor_pos,
507
+ scroll_pos: @scroll_pos,
508
+ node_children: @node_children.dup,
509
+ lines: @lines.dup,
510
+ visible_lines: @visible_lines.dup
511
+ })
512
+
513
+ # Create new filtered tree starting from the selected call
514
+ new_tree = extract_subtree(actual_index)
515
+ return if new_tree.empty?
516
+
517
+ # Update tree and reset state
518
+ @tree = new_tree
519
+ @expanded_nodes = Set.new
520
+ @node_children = {}
521
+ @cursor_pos = 0
522
+ @scroll_pos = 0
523
+
524
+ # Rebuild tree structure and expand all
525
+ analyze_tree_structure
526
+ expand_all_initially
527
+ prepare_lines
528
+ update_visible_lines
529
+ end
530
+
531
+ def go_back
532
+ return if @tree_stack.empty?
533
+
534
+ # Restore previous state (including computed state to avoid expensive recomputation)
535
+ previous_state = @tree_stack.pop
536
+ @tree = previous_state[:tree]
537
+ @expanded_nodes = previous_state[:expanded_nodes]
538
+ @cursor_pos = previous_state[:cursor_pos]
539
+ @scroll_pos = previous_state[:scroll_pos]
540
+ @node_children = previous_state[:node_children] || {}
541
+ @lines = previous_state[:lines] || []
542
+ @visible_lines = previous_state[:visible_lines] || []
543
+
544
+ # Only rebuild if cached state is missing (for backward compatibility)
545
+ if @node_children.empty? || @lines.empty?
546
+ analyze_tree_structure
547
+ prepare_lines
548
+ update_visible_lines
549
+ end
550
+ end
551
+
552
+ def extract_subtree(root_index)
553
+ return [] unless has_children?(root_index)
554
+
555
+ root_node = @tree[root_index]
556
+ children_indices = @node_children[root_index]
557
+
558
+ # Create new tree with adjusted depths
559
+ new_tree = []
560
+ root_depth = root_node.depth
561
+
562
+ # Add the root call
563
+ new_tree << TreeNode.new(
564
+ root_node.event,
565
+ root_node.method_name,
566
+ root_node.parameters,
567
+ root_node.return_value,
568
+ 0, # New root starts at depth 0
569
+ root_node.defined_class,
570
+ root_node.path,
571
+ root_node.lineno,
572
+ root_node.start_time,
573
+ root_node.end_time
574
+ )
575
+
576
+ # Add all children with adjusted depths
577
+ children_indices.each do |child_index|
578
+ child_node = @tree[child_index]
579
+ new_depth = child_node.depth - root_depth
580
+
581
+ new_tree << TreeNode.new(
582
+ child_node.event,
583
+ child_node.method_name,
584
+ child_node.parameters,
585
+ child_node.return_value,
586
+ new_depth,
587
+ child_node.defined_class,
588
+ child_node.path,
589
+ child_node.lineno,
590
+ child_node.start_time,
591
+ child_node.end_time
592
+ )
593
+ end
594
+
595
+ # Find and add the return event for the root call
596
+ return_index = find_matching_return(root_index)
597
+ if return_index
598
+ return_node = @tree[return_index]
599
+ new_tree << TreeNode.new(
600
+ return_node.event,
601
+ return_node.method_name,
602
+ return_node.parameters,
603
+ return_node.return_value,
604
+ 0, # Return at same depth as root call
605
+ return_node.defined_class,
606
+ return_node.path,
607
+ return_node.lineno,
608
+ return_node.start_time,
609
+ return_node.end_time
610
+ )
611
+ end
612
+
613
+ new_tree
614
+ end
615
+
616
+ def find_matching_return(call_index)
617
+ root_node = @tree[call_index]
618
+ return nil if root_node.event != :call
619
+
620
+ # Look for the return event at the same depth with the same method name
621
+ # It should come after all the children
622
+ children_indices = @node_children[call_index]
623
+ search_start = children_indices.any? ? children_indices.max + 1 : call_index + 1
624
+
625
+ (search_start...@tree.length).each do |i|
626
+ node = @tree[i]
627
+ if node.event == :return &&
628
+ node.method_name == root_node.method_name &&
629
+ node.depth == root_node.depth # Return events are at the same depth as their call
630
+ return i
631
+ end
632
+ # Stop if we encounter a node at a shallower depth (we've gone too far)
633
+ break if node.depth < root_node.depth
634
+ end
635
+
636
+ nil
637
+ end
638
+
639
+ def expand_all
640
+ @node_children.each_key do |index|
641
+ @expanded_nodes.add(index) if has_children?(index)
642
+ end
643
+ update_visible_lines
644
+
645
+ # Adjust cursor if it's beyond the new visible range
646
+ if @cursor_pos >= @visible_lines.size
647
+ @cursor_pos = [@visible_lines.size - 1, 0].max
648
+ end
649
+ end
650
+
651
+ def collapse_all
652
+ @expanded_nodes.clear
653
+ update_visible_lines
654
+
655
+ # Adjust cursor if it's beyond the new visible range
656
+ if @cursor_pos >= @visible_lines.size
657
+ @cursor_pos = [@visible_lines.size - 1, 0].max
658
+ end
659
+ end
660
+
661
+ def move_up
662
+ new_pos = @cursor_pos - 1
663
+
664
+ # Skip return events in visible lines
665
+ while new_pos >= 0
666
+ actual_index = @visible_lines[new_pos]
667
+ break if @tree[actual_index].event != :return
668
+ new_pos -= 1
669
+ end
670
+
671
+ @cursor_pos = new_pos if new_pos >= 0
672
+ end
673
+
674
+ def move_down
675
+ new_pos = @cursor_pos + 1
676
+
677
+ # Skip return events in visible lines
678
+ while new_pos < @visible_lines.size
679
+ actual_index = @visible_lines[new_pos]
680
+ break if @tree[actual_index].event != :return
681
+ new_pos += 1
682
+ end
683
+
684
+ @cursor_pos = new_pos if new_pos < @visible_lines.size
685
+ end
686
+ end
687
+ end