Class: Fatty::InputBuffer
- Inherits:
-
Object
- Object
- Fatty::InputBuffer
- Includes:
- Actionable
- Defined in:
- lib/fatty/input_buffer.rb
Overview
The InputBuffer class maintains an editable line of input together with a cursor position. It is responsible only for text editing semantics — inserting and deleting characters, moving the cursor, and reporting the current contents of the buffer.
InputBuffer has no knowledge of keybindings, terminal I/O, history, or rendering. Higher-level components (such as InputField and Terminal) translate user actions into editing operations on the buffer.
The buffer is conceptually a single line of text. Newlines are not interpreted specially and are treated as ordinary characters if present.
Responsibilities:
- Maintain the current text and cursor position
- Insert text at the cursor
- Delete characters before or after the cursor
- Move the cursor within valid bounds
- Replace or clear the buffer contents
Non-responsibilities:
- Keyboard decoding or modifier interpretation
- Command history navigation
- Screen layout or cursor rendering
- Validation of user input
The cursor position represents a location between characters. It is always an integer between 0 and text.length, inclusive. All editing operations must preserve this invariant.
Constant Summary collapse
- DEFAULT_WORD_CHAR_RE =
"[[:alnum:]_]"
Instance Attribute Summary collapse
-
#cursor ⇒ Object
Returns the value of attribute cursor.
-
#kill_ring ⇒ Object
readonly
Returns the value of attribute kill_ring.
-
#mark ⇒ Object
readonly
Returns the value of attribute mark.
-
#text ⇒ Object
Returns the value of attribute text.
-
#undo_stack ⇒ Object
readonly
Returns the value of attribute undo_stack.
-
#virtual_suffix ⇒ Object
Returns the value of attribute virtual_suffix.
-
#word_char_re ⇒ Object
Returns the value of attribute word_char_re.
Instance Method Summary collapse
- #accept_virtual_suffix! ⇒ Object
- #bol? ⇒ Boolean
- #can_redo? ⇒ Boolean
- #can_undo? ⇒ Boolean
- #completion_prefix(from = cursor) ⇒ Object
-
#completion_replace_range(from = cursor) ⇒ Object
Return the Range of the buffer that a completion should replace when the completion is accepted.
-
#countable?(name) ⇒ Boolean
Return whether the action with name takes a count: parameter.
- #delete_range(range) ⇒ Object
-
#display_width ⇒ Object
:category: Utilities.
-
#empty? ⇒ Boolean
:category: Queries.
- #eol? ⇒ Boolean
-
#initialize(word_char_re: DEFAULT_WORD_CHAR_RE, undo_limit: 1_000, kill_ring_max: 60) ⇒ InputBuffer
constructor
A new instance of InputBuffer.
- #length ⇒ Object
- #redo ⇒ Object
- #region_active? ⇒ Boolean
- #region_range ⇒ Object
-
#replace_range(range, str) ⇒ Object
Replace the text in the given Range with str; move cursor to end of insertion.
-
#replace_region(str) ⇒ Object
Replace the region with the str.
-
#replace_span(start, length, str) ⇒ Object
Replace a from start, length characters; move cursor to end of insertion.
- #text_after_cursor ⇒ Object
- #text_before_cursor ⇒ Object
-
#to_s ⇒ Object
(also: #inspect)
:category: Inspect.
-
#undo ⇒ Object
:category: Undo and Redo helpers.
- #undo_size ⇒ Object
- #virtual_length ⇒ Object
- #virtual_text ⇒ Object
-
#word_at_point_range(from = cursor) ⇒ Object
Return a Range that corresponds to the whole word that is "around" the cursor.
Methods included from Actionable
Constructor Details
#initialize(word_char_re: DEFAULT_WORD_CHAR_RE, undo_limit: 1_000, kill_ring_max: 60) ⇒ InputBuffer
Returns a new instance of InputBuffer.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/fatty/input_buffer.rb', line 42 def initialize(word_char_re: DEFAULT_WORD_CHAR_RE, undo_limit: 1_000, kill_ring_max: 60) @text = +"" @virtual_suffix = +"" @cursor = 0 @mark = nil # word_char_re is a fragment like "[[:alnum:]_]" or "[[:alnum:]_-]" # meant to match a single charachter that should be considered a part of # a word for purposes of cursor movement. @word_char_re = normalize_word_char_re(word_char_re) @undo_limit = undo_limit @undo_stack = [] @redo_stack = [] @undo_chain = nil @kill_ring = [] @kill_ring_max = kill_ring_max @last_yank_len = 0 @last_action = nil end |
Instance Attribute Details
#cursor ⇒ Object
Returns the value of attribute cursor.
38 39 40 |
# File 'lib/fatty/input_buffer.rb', line 38 def cursor @cursor end |
#kill_ring ⇒ Object (readonly)
Returns the value of attribute kill_ring.
37 38 39 |
# File 'lib/fatty/input_buffer.rb', line 37 def kill_ring @kill_ring end |
#mark ⇒ Object (readonly)
Returns the value of attribute mark.
37 38 39 |
# File 'lib/fatty/input_buffer.rb', line 37 def mark @mark end |
#text ⇒ Object
Returns the value of attribute text.
38 39 40 |
# File 'lib/fatty/input_buffer.rb', line 38 def text @text end |
#undo_stack ⇒ Object (readonly)
Returns the value of attribute undo_stack.
37 38 39 |
# File 'lib/fatty/input_buffer.rb', line 37 def undo_stack @undo_stack end |
#virtual_suffix ⇒ Object
Returns the value of attribute virtual_suffix.
38 39 40 |
# File 'lib/fatty/input_buffer.rb', line 38 def virtual_suffix @virtual_suffix end |
#word_char_re ⇒ Object
Returns the value of attribute word_char_re.
38 39 40 |
# File 'lib/fatty/input_buffer.rb', line 38 def word_char_re @word_char_re end |
Instance Method Details
#accept_virtual_suffix! ⇒ Object
707 708 709 710 711 712 713 |
# File 'lib/fatty/input_buffer.rb', line 707 def accept_virtual_suffix! return if @virtual_suffix.to_s.empty? @text = virtual_text @cursor = @text.length @virtual_suffix = +"" end |
#bol? ⇒ Boolean
108 109 110 |
# File 'lib/fatty/input_buffer.rb', line 108 def bol? @cursor.zero? end |
#can_redo? ⇒ Boolean
128 129 130 |
# File 'lib/fatty/input_buffer.rb', line 128 def can_redo? !@redo_stack.empty? end |
#can_undo? ⇒ Boolean
124 125 126 |
# File 'lib/fatty/input_buffer.rb', line 124 def can_undo? !@undo_stack.empty? end |
#completion_prefix(from = cursor) ⇒ Object
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 |
# File 'lib/fatty/input_buffer.rb', line 685 def completion_prefix(from = cursor) if region_active? r = region_range r ? text[r].to_s : "" else chars = text.chars if from.positive? && !chars.empty? if from < chars.length && word_char?(chars[from]) r = word_at_point_range(from) r ? text[r.begin...from].to_s : "" elsif word_char?(chars[from - 1]) r = word_span_backward(from) r ? text[r].to_s : "" else "" end else "" end end end |
#completion_replace_range(from = cursor) ⇒ Object
Return the Range of the buffer that a completion should replace when the completion is accepted.
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 |
# File 'lib/fatty/input_buffer.rb', line 657 def completion_replace_range(from = cursor) if region_active? r = region_range return r if r end prefix = completion_prefix(from) chars = text.chars at_word = from < chars.length && word_char?(chars[from]) unless prefix.empty? if at_word return word_at_point_range(from) end back = word_span_backward(from) return back if back.begin < back.end end if at_word fwd = word_span_forward(from) return fwd.end...fwd.end end from...from end |
#countable?(name) ⇒ Boolean
Return whether the action with name takes a count: parameter.
151 152 153 154 155 156 157 158 159 |
# File 'lib/fatty/input_buffer.rb', line 151 def countable?(name) return false unless respond_to?(name) params = method(name).parameters params.any? { |kind, key| kind == :key && key == :count } || params.any? { |kind, key| kind == :keyreq && key == :count } rescue NameError false end |
#delete_range(range) ⇒ Object
610 611 612 613 614 615 616 617 618 619 |
# File 'lib/fatty/input_buffer.rb', line 610 def delete_range(range) r = clamp_range(range) raise ArgumentError, "range required" unless r.is_a?(Range) raise ArgumentError, "range must have begin and end" if r.begin.nil? || r.end.nil? return "" if r.begin == r.end deleted = text[r] || "" replace_range(r, "") deleted end |
#display_width ⇒ Object
:category: Utilities
559 560 561 |
# File 'lib/fatty/input_buffer.rb', line 559 def display_width Unicode::DisplayWidth.of(text) end |
#empty? ⇒ Boolean
:category: Queries
100 101 102 |
# File 'lib/fatty/input_buffer.rb', line 100 def empty? text.empty? end |
#eol? ⇒ Boolean
112 113 114 |
# File 'lib/fatty/input_buffer.rb', line 112 def eol? @cursor == @text.length end |
#length ⇒ Object
104 105 106 |
# File 'lib/fatty/input_buffer.rb', line 104 def length @text.length end |
#redo ⇒ Object
549 550 551 552 553 554 555 |
# File 'lib/fatty/input_buffer.rb', line 549 def redo return if @redo_stack.empty? break_undo_chain! @undo_stack << snapshot restore(@redo_stack.pop) end |
#region_active? ⇒ Boolean
136 137 138 |
# File 'lib/fatty/input_buffer.rb', line 136 def region_active? !!@mark && @mark != @cursor end |
#region_range ⇒ Object
140 141 142 143 144 145 146 147 148 |
# File 'lib/fatty/input_buffer.rb', line 140 def region_range if region_active? a = @mark b = @cursor s = [a, b].min e = [a, b].max clamp_range(s...e) end end |
#replace_range(range, str) ⇒ Object
Replace the text in the given Range with str; move cursor to end of insertion
584 585 586 587 588 589 590 591 592 593 594 |
# File 'lib/fatty/input_buffer.rb', line 584 def replace_range(range, str) r = range raise ArgumentError, "range required" unless r.is_a?(Range) raise ArgumentError, "range must have begin and end" if r.begin.nil? || r.end.nil? start = r.begin.to_i finish = r.end.to_i finish += 1 unless r.exclude_end? replace_span(start, finish - start, str) end |
#replace_region(str) ⇒ Object
Replace the region with the str
597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'lib/fatty/input_buffer.rb', line 597 def replace_region(str) s = str.to_s r = region_range if r && r.begin < r.end replace_span(r.begin, r.end - r.begin, s) @mark = nil else text.insert(@cursor, s) @cursor += s.length end s end |
#replace_span(start, length, str) ⇒ Object
Replace a from start, length characters; move cursor to end of insertion
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 |
# File 'lib/fatty/input_buffer.rb', line 564 def replace_span(start, length, str) start = start.to_i length = length.to_i str = str.to_s start = 0 if start.negative? start = text.length if start > text.length length = 0 if length.negative? max_len = text.length - start length = max_len if length > max_len text.slice!(start, length) if length.positive? text.insert(start, str) unless str.empty? @cursor = start + str.length adjust_mark_for_replace_span!(start, length, str.length) end |
#text_after_cursor ⇒ Object
120 121 122 |
# File 'lib/fatty/input_buffer.rb', line 120 def text_after_cursor text[@cursor..] || "" end |
#text_before_cursor ⇒ Object
116 117 118 |
# File 'lib/fatty/input_buffer.rb', line 116 def text_before_cursor text[0, @cursor] || "" end |
#to_s ⇒ Object Also known as: inspect
:category: Inspect
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/fatty/input_buffer.rb', line 64 def to_s text_w_cursor = if mark if mark < cursor "#{text[0..mark - 1]}[#{text[mark..cursor - 1]}]|#{text[cursor..]}" elsif mark > cursor "#{text[0..cursor - 1]}[|#{text[cursor..mark - 1]}]#{text[mark..]}" else # They're equal, ignore mark "#{text[0..cursor - 1]}|#{text[cursor..]}]" end elsif cursor > 0 "#{text[0..cursor - 1]}|#{text[cursor..]}" else "|#{text}" end v_text = if virtual_suffix.empty? '' else "(#{virtual_suffix})" end "<InputBuffer:#{object_id}> <#{text_w_cursor}>#{v_text} => Kill[#{kill_ring.size}] => Undo[#{undo_stack.size}]" end |
#undo ⇒ Object
:category: Undo and Redo helpers
541 542 543 544 545 546 547 |
# File 'lib/fatty/input_buffer.rb', line 541 def undo return if @undo_stack.empty? break_undo_chain! @redo_stack << snapshot restore(@undo_stack.pop) end |
#undo_size ⇒ Object
132 133 134 |
# File 'lib/fatty/input_buffer.rb', line 132 def undo_size @undo_stack.size end |
#virtual_length ⇒ Object
94 95 96 |
# File 'lib/fatty/input_buffer.rb', line 94 def virtual_length virtual_text.length end |
#virtual_text ⇒ Object
90 91 92 |
# File 'lib/fatty/input_buffer.rb', line 90 def virtual_text @text + @virtual_suffix.to_s end |
#word_at_point_range(from = cursor) ⇒ Object
Return a Range that corresponds to the whole word that is "around" the cursor. The whole region if region active, the word cursor is on a if it's on a word, or nothing otherwise.
624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 |
# File 'lib/fatty/input_buffer.rb', line 624 def word_at_point_range(from = cursor) if region_active? a, b = region_range.minmax return a...b end chars = text.chars return from...from if from < 0 || from > chars.length return from...from if chars.empty? on_word = if from == chars.length from.positive? && word_char?(chars[from - 1]) else word_char?(chars[from]) end return from...from unless on_word left = from if left == chars.length left -= 1 end left -= 1 while left.positive? && word_char?(chars[left - 1]) right = from right += 1 while right < chars.length && word_char?(chars[right]) left...right end |