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_CHARS =
"[[: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_re ⇒ Object
Returns the value of attribute word_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_chars: DEFAULT_WORD_CHARS, word_re: nil, 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_chars: DEFAULT_WORD_CHARS, word_re: nil, 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 61 62 63 |
# File 'lib/fatty/input_buffer.rb', line 42 def initialize(word_chars: DEFAULT_WORD_CHARS, word_re: nil, undo_limit: 1_000, kill_ring_max: 60) @text = +"" @virtual_suffix = +"" @cursor = 0 @mark = nil @word_re = if word_re word_re else # word_chars is a fragment like "[[:alnum:]_]" or "[[:alnum:]_-]" Regexp.new(word_chars) end @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_re ⇒ Object
Returns the value of attribute word_re.
38 39 40 |
# File 'lib/fatty/input_buffer.rb', line 38 def word_re @word_re end |
Instance Method Details
#accept_virtual_suffix! ⇒ Object
700 701 702 703 704 705 706 |
# File 'lib/fatty/input_buffer.rb', line 700 def accept_virtual_suffix! return if @virtual_suffix.to_s.empty? @text = virtual_text @cursor = @text.length @virtual_suffix = +"" end |
#bol? ⇒ Boolean
111 112 113 |
# File 'lib/fatty/input_buffer.rb', line 111 def bol? @cursor.zero? end |
#can_redo? ⇒ Boolean
131 132 133 |
# File 'lib/fatty/input_buffer.rb', line 131 def can_redo? !@redo_stack.empty? end |
#can_undo? ⇒ Boolean
127 128 129 |
# File 'lib/fatty/input_buffer.rb', line 127 def can_undo? !@undo_stack.empty? end |
#completion_prefix(from = cursor) ⇒ Object
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 |
# File 'lib/fatty/input_buffer.rb', line 678 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.
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/fatty/input_buffer.rb', line 650 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.
154 155 156 157 158 159 160 161 162 |
# File 'lib/fatty/input_buffer.rb', line 154 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
603 604 605 606 607 608 609 610 611 612 |
# File 'lib/fatty/input_buffer.rb', line 603 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
552 553 554 |
# File 'lib/fatty/input_buffer.rb', line 552 def display_width Unicode::DisplayWidth.of(text) end |
#empty? ⇒ Boolean
:category: Queries
103 104 105 |
# File 'lib/fatty/input_buffer.rb', line 103 def empty? text.empty? end |
#eol? ⇒ Boolean
115 116 117 |
# File 'lib/fatty/input_buffer.rb', line 115 def eol? @cursor == @text.length end |
#length ⇒ Object
107 108 109 |
# File 'lib/fatty/input_buffer.rb', line 107 def length @text.length end |
#redo ⇒ Object
542 543 544 545 546 547 548 |
# File 'lib/fatty/input_buffer.rb', line 542 def redo return if @redo_stack.empty? break_undo_chain! @undo_stack << snapshot restore(@redo_stack.pop) end |
#region_active? ⇒ Boolean
139 140 141 |
# File 'lib/fatty/input_buffer.rb', line 139 def region_active? !!@mark && @mark != @cursor end |
#region_range ⇒ Object
143 144 145 146 147 148 149 150 151 |
# File 'lib/fatty/input_buffer.rb', line 143 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
577 578 579 580 581 582 583 584 585 586 587 |
# File 'lib/fatty/input_buffer.rb', line 577 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
590 591 592 593 594 595 596 597 598 599 600 601 |
# File 'lib/fatty/input_buffer.rb', line 590 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
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 |
# File 'lib/fatty/input_buffer.rb', line 557 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
123 124 125 |
# File 'lib/fatty/input_buffer.rb', line 123 def text_after_cursor text[@cursor..] || "" end |
#text_before_cursor ⇒ Object
119 120 121 |
# File 'lib/fatty/input_buffer.rb', line 119 def text_before_cursor text[0, @cursor] || "" end |
#to_s ⇒ Object Also known as: inspect
:category: Inspect
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/fatty/input_buffer.rb', line 67 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
534 535 536 537 538 539 540 |
# File 'lib/fatty/input_buffer.rb', line 534 def undo return if @undo_stack.empty? break_undo_chain! @redo_stack << snapshot restore(@undo_stack.pop) end |
#undo_size ⇒ Object
135 136 137 |
# File 'lib/fatty/input_buffer.rb', line 135 def undo_size @undo_stack.size end |
#virtual_length ⇒ Object
97 98 99 |
# File 'lib/fatty/input_buffer.rb', line 97 def virtual_length virtual_text.length end |
#virtual_text ⇒ Object
93 94 95 |
# File 'lib/fatty/input_buffer.rb', line 93 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.
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 |
# File 'lib/fatty/input_buffer.rb', line 617 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 |