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_CHARS =
"[[:alnum:]_]"

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Actionable

included

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

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

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

Returns:

  • (Boolean)


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

def bol?
  @cursor.zero?
end

#can_redo?Boolean

Returns:

  • (Boolean)


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

def can_redo?
  !@redo_stack.empty?
end

#can_undo?Boolean

Returns:

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

Returns:

  • (Boolean)


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

Raises:

  • (ArgumentError)


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_widthObject

: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

Returns:

  • (Boolean)


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

def empty?
  text.empty?
end

#eol?Boolean

Returns:

  • (Boolean)


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

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

#lengthObject



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

def length
  @text.length
end

#redoObject



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

Returns:

  • (Boolean)


139
140
141
# File 'lib/fatty/input_buffer.rb', line 139

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

#region_rangeObject



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

Raises:

  • (ArgumentError)


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_cursorObject



123
124
125
# File 'lib/fatty/input_buffer.rb', line 123

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

#text_before_cursorObject



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

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

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

#undoObject

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



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

def undo_size
  @undo_stack.size
end

#virtual_lengthObject



97
98
99
# File 'lib/fatty/input_buffer.rb', line 97

def virtual_length
  virtual_text.length
end

#virtual_textObject



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