Class: Fatty::InputBuffer

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Methods included from Actionable

included

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

#cursorObject

Returns the value of attribute cursor.



38
39
40
# File 'lib/fatty/input_buffer.rb', line 38

def cursor
  @cursor
end

#kill_ringObject (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

#markObject (readonly)

Returns the value of attribute mark.



37
38
39
# File 'lib/fatty/input_buffer.rb', line 37

def mark
  @mark
end

#textObject

Returns the value of attribute text.



38
39
40
# File 'lib/fatty/input_buffer.rb', line 38

def text
  @text
end

#undo_stackObject (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_suffixObject

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_reObject

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

Returns:

  • (Boolean)


108
109
110
# File 'lib/fatty/input_buffer.rb', line 108

def bol?
  @cursor.zero?
end

#can_redo?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/fatty/input_buffer.rb', line 128

def can_redo?
  !@redo_stack.empty?
end

#can_undo?Boolean

Returns:

  • (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.

Returns:

  • (Boolean)


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

Raises:

  • (ArgumentError)


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_widthObject

: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

Returns:

  • (Boolean)


100
101
102
# File 'lib/fatty/input_buffer.rb', line 100

def empty?
  text.empty?
end

#eol?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/fatty/input_buffer.rb', line 112

def eol?
  @cursor == @text.length
end

#lengthObject



104
105
106
# File 'lib/fatty/input_buffer.rb', line 104

def length
  @text.length
end

#redoObject



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

Returns:

  • (Boolean)


136
137
138
# File 'lib/fatty/input_buffer.rb', line 136

def region_active?
  !!@mark && @mark != @cursor
end

#region_rangeObject



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

Raises:

  • (ArgumentError)


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_cursorObject



120
121
122
# File 'lib/fatty/input_buffer.rb', line 120

def text_after_cursor
  text[@cursor..] || ""
end

#text_before_cursorObject



116
117
118
# File 'lib/fatty/input_buffer.rb', line 116

def text_before_cursor
  text[0, @cursor] || ""
end

#to_sObject 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

#undoObject

: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_sizeObject



132
133
134
# File 'lib/fatty/input_buffer.rb', line 132

def undo_size
  @undo_stack.size
end

#virtual_lengthObject



94
95
96
# File 'lib/fatty/input_buffer.rb', line 94

def virtual_length
  virtual_text.length
end

#virtual_textObject



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