fatty 0.99.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.
- checksums.yaml +7 -0
- data/.envrc +2 -0
- data/.simplecov +23 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +34 -0
- data/CHANGELOG.org +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/README.org +166 -0
- data/Rakefile +15 -0
- data/TODO.org +163 -0
- data/examples/markdown/native-markdown.md +370 -0
- data/examples/markdown/ox-gfm-markdown.md +373 -0
- data/examples/markdown/ox-gfm-markdown.org +376 -0
- data/exe/fatty +275 -0
- data/fatty.gemspec +42 -0
- data/lib/fatty/accept_env.rb +32 -0
- data/lib/fatty/action.rb +103 -0
- data/lib/fatty/action_environment.rb +42 -0
- data/lib/fatty/actionable.rb +73 -0
- data/lib/fatty/alert.rb +93 -0
- data/lib/fatty/ansi/renderer.rb +168 -0
- data/lib/fatty/ansi.rb +352 -0
- data/lib/fatty/colors/color.rb +379 -0
- data/lib/fatty/colors/pairs.rb +73 -0
- data/lib/fatty/colors/palette.rb +73 -0
- data/lib/fatty/colors/rgb.txt +788 -0
- data/lib/fatty/colors.rb +5 -0
- data/lib/fatty/config.rb +86 -0
- data/lib/fatty/config_files/config.yml +50 -0
- data/lib/fatty/config_files/help.md +120 -0
- data/lib/fatty/config_files/help.org +124 -0
- data/lib/fatty/config_files/keybindings.yml +49 -0
- data/lib/fatty/config_files/keydefs.yml +23 -0
- data/lib/fatty/config_files/themes/mono.yml +76 -0
- data/lib/fatty/config_files/themes/nordic.yml +77 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
- data/lib/fatty/config_files/themes/terminal.yml +90 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
- data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
- data/lib/fatty/core_ext/string.rb +21 -0
- data/lib/fatty/core_ext.rb +3 -0
- data/lib/fatty/counter.rb +81 -0
- data/lib/fatty/curses/context.rb +279 -0
- data/lib/fatty/curses/curses_coder.rb +684 -0
- data/lib/fatty/curses/event_source.rb +230 -0
- data/lib/fatty/curses/key_decoder.rb +183 -0
- data/lib/fatty/curses/patch.rb +116 -0
- data/lib/fatty/curses/window_styling.rb +32 -0
- data/lib/fatty/curses.rb +16 -0
- data/lib/fatty/env.rb +100 -0
- data/lib/fatty/help.rb +41 -0
- data/lib/fatty/history/entry.rb +71 -0
- data/lib/fatty/history.rb +289 -0
- data/lib/fatty/input_buffer.rb +998 -0
- data/lib/fatty/input_field.rb +507 -0
- data/lib/fatty/key_event.rb +342 -0
- data/lib/fatty/key_map.rb +392 -0
- data/lib/fatty/keymaps/emacs.rb +189 -0
- data/lib/fatty/log_formats/json.rb +47 -0
- data/lib/fatty/log_formats/text.rb +67 -0
- data/lib/fatty/logger.rb +142 -0
- data/lib/fatty/markdown/ansi_renderer.rb +373 -0
- data/lib/fatty/markdown/render.rb +22 -0
- data/lib/fatty/markdown.rb +4 -0
- data/lib/fatty/menu_env.rb +22 -0
- data/lib/fatty/mouse_event.rb +32 -0
- data/lib/fatty/output_buffer.rb +78 -0
- data/lib/fatty/pager.rb +801 -0
- data/lib/fatty/prompt.rb +40 -0
- data/lib/fatty/renderer/curses.rb +697 -0
- data/lib/fatty/renderer/truecolor.rb +607 -0
- data/lib/fatty/renderer.rb +419 -0
- data/lib/fatty/screen.rb +96 -0
- data/lib/fatty/search.rb +43 -0
- data/lib/fatty/session/alert_session.rb +52 -0
- data/lib/fatty/session/input_session.rb +99 -0
- data/lib/fatty/session/isearch_session.rb +172 -0
- data/lib/fatty/session/keytest_session.rb +236 -0
- data/lib/fatty/session/modal_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +105 -0
- data/lib/fatty/session/popup_session.rb +540 -0
- data/lib/fatty/session/prompt_session.rb +157 -0
- data/lib/fatty/session/search_session.rb +136 -0
- data/lib/fatty/session/shell_session.rb +566 -0
- data/lib/fatty/session.rb +173 -0
- data/lib/fatty/sessions.rb +14 -0
- data/lib/fatty/terminal/popup_owner.rb +26 -0
- data/lib/fatty/terminal/progress.rb +374 -0
- data/lib/fatty/terminal.rb +1067 -0
- data/lib/fatty/themes/loader.rb +136 -0
- data/lib/fatty/themes/manager.rb +71 -0
- data/lib/fatty/themes/registry.rb +64 -0
- data/lib/fatty/themes/resolver.rb +224 -0
- data/lib/fatty/themes/themes.rb +131 -0
- data/lib/fatty/themes.rb +6 -0
- data/lib/fatty/version.rb +5 -0
- data/lib/fatty/view/alert_view.rb +14 -0
- data/lib/fatty/view/cursor_view.rb +18 -0
- data/lib/fatty/view/input_view.rb +9 -0
- data/lib/fatty/view/output_view.rb +9 -0
- data/lib/fatty/view/status_view.rb +14 -0
- data/lib/fatty/view.rb +33 -0
- data/lib/fatty/viewport.rb +90 -0
- data/lib/fatty/views.rb +9 -0
- data/lib/fatty.rb +55 -0
- data/sig/fatty.rbs +4 -0
- metadata +250 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "unicode/display_width"
|
|
4
|
+
|
|
5
|
+
module Fatty
|
|
6
|
+
# The InputBuffer class maintains an editable line of input together with a
|
|
7
|
+
# cursor position. It is responsible only for text editing semantics —
|
|
8
|
+
# inserting and deleting characters, moving the cursor, and reporting the
|
|
9
|
+
# current contents of the buffer.
|
|
10
|
+
#
|
|
11
|
+
# InputBuffer has no knowledge of keybindings, terminal I/O, history, or
|
|
12
|
+
# rendering. Higher-level components (such as InputField and Terminal)
|
|
13
|
+
# translate user actions into editing operations on the buffer.
|
|
14
|
+
#
|
|
15
|
+
# The buffer is conceptually a single line of text. Newlines are not
|
|
16
|
+
# interpreted specially and are treated as ordinary characters if present.
|
|
17
|
+
#
|
|
18
|
+
# Responsibilities:
|
|
19
|
+
# - Maintain the current text and cursor position
|
|
20
|
+
# - Insert text at the cursor
|
|
21
|
+
# - Delete characters before or after the cursor
|
|
22
|
+
# - Move the cursor within valid bounds
|
|
23
|
+
# - Replace or clear the buffer contents
|
|
24
|
+
#
|
|
25
|
+
# Non-responsibilities:
|
|
26
|
+
# - Keyboard decoding or modifier interpretation
|
|
27
|
+
# - Command history navigation
|
|
28
|
+
# - Screen layout or cursor rendering
|
|
29
|
+
# - Validation of user input
|
|
30
|
+
#
|
|
31
|
+
# The cursor position represents a location *between* characters. It is
|
|
32
|
+
# always an integer between 0 and text.length, inclusive. All editing
|
|
33
|
+
# operations must preserve this invariant.
|
|
34
|
+
class InputBuffer
|
|
35
|
+
include Actionable
|
|
36
|
+
|
|
37
|
+
attr_reader :mark, :kill_ring, :undo_stack
|
|
38
|
+
attr_accessor :text, :cursor, :word_re, :virtual_suffix
|
|
39
|
+
|
|
40
|
+
DEFAULT_WORD_CHARS = "[[:alnum:]_]"
|
|
41
|
+
|
|
42
|
+
def initialize(word_chars: DEFAULT_WORD_CHARS, word_re: nil, undo_limit: 1_000, kill_ring_max: 60)
|
|
43
|
+
@text = +""
|
|
44
|
+
@virtual_suffix = +""
|
|
45
|
+
@cursor = 0
|
|
46
|
+
@mark = nil
|
|
47
|
+
@word_re =
|
|
48
|
+
if word_re
|
|
49
|
+
word_re
|
|
50
|
+
else
|
|
51
|
+
# word_chars is a fragment like "[[:alnum:]_]" or "[[:alnum:]_-]"
|
|
52
|
+
Regexp.new(word_chars)
|
|
53
|
+
end
|
|
54
|
+
@undo_limit = undo_limit
|
|
55
|
+
@undo_stack = []
|
|
56
|
+
@redo_stack = []
|
|
57
|
+
@undo_chain = nil
|
|
58
|
+
|
|
59
|
+
@kill_ring = []
|
|
60
|
+
@kill_ring_max = kill_ring_max
|
|
61
|
+
@last_yank_len = 0
|
|
62
|
+
@last_action = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# :category: Inspect
|
|
66
|
+
|
|
67
|
+
def to_s
|
|
68
|
+
text_w_cursor =
|
|
69
|
+
if mark
|
|
70
|
+
if mark < cursor
|
|
71
|
+
"#{text[0..mark - 1]}[#{text[mark..cursor - 1]}]|#{text[cursor..]}"
|
|
72
|
+
elsif mark > cursor
|
|
73
|
+
"#{text[0..cursor - 1]}[|#{text[cursor..mark - 1]}]#{text[mark..]}"
|
|
74
|
+
else
|
|
75
|
+
# They're equal, ignore mark
|
|
76
|
+
"#{text[0..cursor - 1]}|#{text[cursor..]}]"
|
|
77
|
+
end
|
|
78
|
+
elsif cursor > 0
|
|
79
|
+
"#{text[0..cursor - 1]}|#{text[cursor..]}"
|
|
80
|
+
else
|
|
81
|
+
"|#{text}"
|
|
82
|
+
end
|
|
83
|
+
v_text =
|
|
84
|
+
if virtual_suffix.empty?
|
|
85
|
+
''
|
|
86
|
+
else
|
|
87
|
+
"(#{virtual_suffix})"
|
|
88
|
+
end
|
|
89
|
+
"<InputBuffer:#{object_id}> <#{text_w_cursor}>#{v_text} => Kill[#{kill_ring.size}] => Undo[#{undo_stack.size}]"
|
|
90
|
+
end
|
|
91
|
+
alias_method :inspect, :to_s
|
|
92
|
+
|
|
93
|
+
def virtual_text
|
|
94
|
+
@text + @virtual_suffix.to_s
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def virtual_length
|
|
98
|
+
virtual_text.length
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :category: Queries
|
|
102
|
+
|
|
103
|
+
def empty?
|
|
104
|
+
text.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def length
|
|
108
|
+
@text.length
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bol?
|
|
112
|
+
@cursor.zero?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def eol?
|
|
116
|
+
@cursor == @text.length
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def text_before_cursor
|
|
120
|
+
text[0, @cursor] || ""
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def text_after_cursor
|
|
124
|
+
text[@cursor..] || ""
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def can_undo?
|
|
128
|
+
!@undo_stack.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def can_redo?
|
|
132
|
+
!@redo_stack.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def undo_size
|
|
136
|
+
@undo_stack.size
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def region_active?
|
|
140
|
+
!!@mark && @mark != @cursor
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def region_range
|
|
144
|
+
if region_active?
|
|
145
|
+
a = @mark
|
|
146
|
+
b = @cursor
|
|
147
|
+
s = [a, b].min
|
|
148
|
+
e = [a, b].max
|
|
149
|
+
clamp_range(s...e)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Return whether the action with name takes a count: parameter.
|
|
154
|
+
def countable?(name)
|
|
155
|
+
return false unless respond_to?(name)
|
|
156
|
+
|
|
157
|
+
params = method(name).parameters
|
|
158
|
+
params.any? { |kind, key| kind == :key && key == :count } ||
|
|
159
|
+
params.any? { |kind, key| kind == :keyreq && key == :count }
|
|
160
|
+
rescue NameError
|
|
161
|
+
false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# category: Actions: Cursor Movement
|
|
165
|
+
|
|
166
|
+
desc "Undo the most recent buffer edit, text and cursor. Returns true/false."
|
|
167
|
+
action :undo do
|
|
168
|
+
undo
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
desc "Redo the most recently undone edit. Returns true/false."
|
|
172
|
+
action :redo do
|
|
173
|
+
redo
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# category: Actions: Cursor Movement
|
|
177
|
+
|
|
178
|
+
desc "Move cursor to the beginning of the line"
|
|
179
|
+
action :bol do
|
|
180
|
+
break_undo_chain!
|
|
181
|
+
@cursor = 0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
desc "Move cursor to the end of the line"
|
|
185
|
+
action :eol do
|
|
186
|
+
break_undo_chain!
|
|
187
|
+
@cursor = virtual_length
|
|
188
|
+
promote_to_cursor!
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
desc "Move cursor count words to the right"
|
|
192
|
+
action :move_word_right do |count: 1|
|
|
193
|
+
break_undo_chain!
|
|
194
|
+
repeat(count) { move_word_right_once }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
desc "Move cursor count words to the left"
|
|
198
|
+
action :move_word_left do |count: 1|
|
|
199
|
+
break_undo_chain!
|
|
200
|
+
repeat(count) { move_word_left_once }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
desc "Move cursor count characters to the left"
|
|
204
|
+
action :move_left do |count: 1|
|
|
205
|
+
break_undo_chain!
|
|
206
|
+
n = normalize_count(count)
|
|
207
|
+
@cursor = [@cursor - n, 0].max
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
desc "Move cursor count characters to the right"
|
|
211
|
+
action :move_right do |count: 1|
|
|
212
|
+
break_undo_chain!
|
|
213
|
+
n = normalize_count(count)
|
|
214
|
+
@cursor = [@cursor + n, virtual_length].min
|
|
215
|
+
promote_to_cursor!
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# :category: Actions: Region
|
|
219
|
+
|
|
220
|
+
desc "Set the mark at the current cursor position (activates region)."
|
|
221
|
+
action :set_mark do
|
|
222
|
+
break_undo_chain!
|
|
223
|
+
@mark = @cursor
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
desc "Clear the mark (deactivates region)."
|
|
227
|
+
action :clear_mark do
|
|
228
|
+
break_undo_chain!
|
|
229
|
+
@mark = nil
|
|
230
|
+
@virtual_suffix = +''
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# :category: Actions: Change Buffer
|
|
234
|
+
|
|
235
|
+
desc "Clear the buffer"
|
|
236
|
+
action :clear do
|
|
237
|
+
return if text.empty? && @cursor.zero?
|
|
238
|
+
|
|
239
|
+
with_undo do
|
|
240
|
+
@mark = nil
|
|
241
|
+
@last_action = nil
|
|
242
|
+
@virtual_suffix = +''
|
|
243
|
+
text.clear
|
|
244
|
+
@cursor = 0
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
desc "Add a count copies of string at the cursor and move cursor after the inserted text"
|
|
249
|
+
action :insert do |str, count: 1|
|
|
250
|
+
s = str.to_s
|
|
251
|
+
n = normalize_count(count)
|
|
252
|
+
return if s.empty?
|
|
253
|
+
|
|
254
|
+
with_undo_coalesced(:insert) do
|
|
255
|
+
@last_action = nil
|
|
256
|
+
payload = (s * n)
|
|
257
|
+
|
|
258
|
+
if region_active?
|
|
259
|
+
r = region_range
|
|
260
|
+
if r && r.begin < r.end
|
|
261
|
+
# Replace the selected text
|
|
262
|
+
text[r] = payload
|
|
263
|
+
@cursor = r.begin + payload.length
|
|
264
|
+
else
|
|
265
|
+
text.insert(@cursor, payload)
|
|
266
|
+
@cursor += payload.length
|
|
267
|
+
end
|
|
268
|
+
clear_mark
|
|
269
|
+
else
|
|
270
|
+
text.insert(@cursor, payload)
|
|
271
|
+
@cursor += payload.length
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
action :self_insert, to: :insert
|
|
276
|
+
|
|
277
|
+
desc "Replace buffer contents with a string and move the cursor to the end"
|
|
278
|
+
action :replace do |str|
|
|
279
|
+
str = str.to_s
|
|
280
|
+
with_undo do
|
|
281
|
+
@last_action = nil
|
|
282
|
+
@text = str.dup
|
|
283
|
+
@virtual_suffix = +''
|
|
284
|
+
@cursor = @text.length
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
action :set, to: :replace
|
|
288
|
+
|
|
289
|
+
desc "Delete the character before the cursor"
|
|
290
|
+
action :delete_char_backward do |count: 1|
|
|
291
|
+
if region_active?
|
|
292
|
+
delete_region
|
|
293
|
+
else
|
|
294
|
+
n = normalize_count(count)
|
|
295
|
+
return if @cursor.zero?
|
|
296
|
+
|
|
297
|
+
with_undo do
|
|
298
|
+
@last_action = nil
|
|
299
|
+
repeat(n) do
|
|
300
|
+
break if @cursor.zero?
|
|
301
|
+
|
|
302
|
+
text.slice!(@cursor - 1)
|
|
303
|
+
@cursor -= 1
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
desc "Delete count characters after the cursor"
|
|
310
|
+
action :delete_char_forward do |count: 1|
|
|
311
|
+
if region_active?
|
|
312
|
+
delete_region
|
|
313
|
+
else
|
|
314
|
+
n = normalize_count(count)
|
|
315
|
+
return if @cursor == text.length
|
|
316
|
+
|
|
317
|
+
with_undo do
|
|
318
|
+
@last_action = nil
|
|
319
|
+
repeat(n) do
|
|
320
|
+
break if @cursor == text.length
|
|
321
|
+
|
|
322
|
+
text.slice!(@cursor, 1)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
desc "Delete to the end of the buffer and return deleted string"
|
|
329
|
+
action :kill_to_eol do
|
|
330
|
+
return "" if eol?
|
|
331
|
+
|
|
332
|
+
killed = ""
|
|
333
|
+
with_undo do
|
|
334
|
+
killed = delete_range(cursor...text.length)
|
|
335
|
+
push_kill(killed)
|
|
336
|
+
@last_action = :kill
|
|
337
|
+
end
|
|
338
|
+
killed
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
desc "Delete to the beginning of the buffer and return deleted string"
|
|
342
|
+
action :kill_to_bol do
|
|
343
|
+
return "" if bol?
|
|
344
|
+
|
|
345
|
+
killed = ""
|
|
346
|
+
with_undo do
|
|
347
|
+
killed = delete_range(0...cursor)
|
|
348
|
+
@cursor = 0
|
|
349
|
+
push_kill(killed)
|
|
350
|
+
@last_action = :kill
|
|
351
|
+
end
|
|
352
|
+
killed
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
desc "Kill count words after the cursor and return the deleted string"
|
|
356
|
+
action :kill_word_forward do |count: 1|
|
|
357
|
+
n = normalize_count(count)
|
|
358
|
+
return "" if eol?
|
|
359
|
+
|
|
360
|
+
deleted = ""
|
|
361
|
+
with_undo do
|
|
362
|
+
start = cursor
|
|
363
|
+
finish = start
|
|
364
|
+
repeat(n) do
|
|
365
|
+
break if finish >= text.length
|
|
366
|
+
|
|
367
|
+
span = word_span_forward(finish)
|
|
368
|
+
break if span.begin == span.end
|
|
369
|
+
|
|
370
|
+
finish = span.end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
deleted = delete_range(start...finish)
|
|
374
|
+
@cursor = start
|
|
375
|
+
push_kill(deleted)
|
|
376
|
+
@last_action = :kill
|
|
377
|
+
end
|
|
378
|
+
deleted
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
desc "Kill count words before the cursor and return the deleted string"
|
|
382
|
+
action :kill_word_backward do |count: 1|
|
|
383
|
+
n = normalize_count(count)
|
|
384
|
+
return "" if bol?
|
|
385
|
+
|
|
386
|
+
deleted = ""
|
|
387
|
+
with_undo do
|
|
388
|
+
finish = cursor
|
|
389
|
+
start = finish
|
|
390
|
+
repeat(n) do
|
|
391
|
+
break if start <= 0
|
|
392
|
+
|
|
393
|
+
span = word_span_backward(start)
|
|
394
|
+
break if span.begin == span.end
|
|
395
|
+
|
|
396
|
+
start = span.begin
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
deleted = delete_range(start...finish)
|
|
400
|
+
@cursor = start
|
|
401
|
+
push_kill(deleted)
|
|
402
|
+
@last_action = :kill
|
|
403
|
+
end
|
|
404
|
+
deleted
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
desc "Transpose the two characters around point."
|
|
408
|
+
action :transpose_chars do |count: 1|
|
|
409
|
+
n = normalize_count(count)
|
|
410
|
+
return if text.length < 2
|
|
411
|
+
|
|
412
|
+
with_undo do
|
|
413
|
+
@last_action = nil
|
|
414
|
+
repeat(n) do
|
|
415
|
+
transpose_chars_once
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
desc "Transpose the word at point with the adjacent word."
|
|
421
|
+
action :transpose_words do |count: 1|
|
|
422
|
+
n = normalize_count(count)
|
|
423
|
+
return if text.empty?
|
|
424
|
+
|
|
425
|
+
with_undo do
|
|
426
|
+
@last_action = nil
|
|
427
|
+
repeat(n) do
|
|
428
|
+
changed = transpose_words_once
|
|
429
|
+
break unless changed
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
desc "Kill the active region and return deleted text; pushes to kill ring."
|
|
435
|
+
action :kill_region do
|
|
436
|
+
r = region_range
|
|
437
|
+
return "" unless r
|
|
438
|
+
|
|
439
|
+
deleted = ""
|
|
440
|
+
with_undo do
|
|
441
|
+
deleted = delete_range(r)
|
|
442
|
+
@cursor = r.begin
|
|
443
|
+
@mark = nil
|
|
444
|
+
push_kill(deleted)
|
|
445
|
+
@last_action = :kill
|
|
446
|
+
end
|
|
447
|
+
deleted
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
desc "Delete the active region without pushing it to the kill ring."
|
|
451
|
+
action :delete_region do
|
|
452
|
+
r = region_range
|
|
453
|
+
return "" unless r
|
|
454
|
+
|
|
455
|
+
deleted = ""
|
|
456
|
+
with_undo do
|
|
457
|
+
deleted = delete_range(r)
|
|
458
|
+
@cursor = r.begin
|
|
459
|
+
@mark = nil
|
|
460
|
+
@last_action = nil
|
|
461
|
+
end
|
|
462
|
+
deleted
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
desc "Copy the active region and return copied text; pushes to kill ring."
|
|
466
|
+
action :copy_region do
|
|
467
|
+
r = region_range
|
|
468
|
+
return "" unless r
|
|
469
|
+
|
|
470
|
+
break_undo_chain!
|
|
471
|
+
copied = text[r] || ""
|
|
472
|
+
@mark = nil
|
|
473
|
+
push_kill(copied)
|
|
474
|
+
@last_action = :copy
|
|
475
|
+
copied
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
desc "Yank (paste) the most recent kill at the cursor."
|
|
479
|
+
action :yank do
|
|
480
|
+
y = @kill_ring.first.to_s
|
|
481
|
+
return "" if y.empty?
|
|
482
|
+
|
|
483
|
+
base = snapshot
|
|
484
|
+
with_undo(before: base) do
|
|
485
|
+
text.insert(@cursor, y)
|
|
486
|
+
@cursor += y.length
|
|
487
|
+
adjust_mark_for_replace_span!(@cursor - y.length, 0, y.length)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
@yank_undo_snapshot = base
|
|
491
|
+
@last_yank_len = y.length
|
|
492
|
+
@last_action = :yank
|
|
493
|
+
y
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
desc "Replace the last yanked text with the previous kill ring entry."
|
|
497
|
+
action :yank_pop do |count: 1|
|
|
498
|
+
return "" unless @last_action == :yank || @last_action == :yank_pop
|
|
499
|
+
return "" if @kill_ring.length < 2
|
|
500
|
+
return "" if @last_yank_len.to_i <= 0
|
|
501
|
+
|
|
502
|
+
break_undo_chain!
|
|
503
|
+
n = normalize_count(count)
|
|
504
|
+
y = ""
|
|
505
|
+
repeat(n) do
|
|
506
|
+
first = @kill_ring.shift
|
|
507
|
+
@kill_ring << first
|
|
508
|
+
y = @kill_ring.first.to_s
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
start = @cursor - @last_yank_len
|
|
512
|
+
start = 0 if start < 0
|
|
513
|
+
replace_span(start, @last_yank_len, y)
|
|
514
|
+
|
|
515
|
+
# We changed buffer state; redo history is no longer valid.
|
|
516
|
+
@redo_stack.clear
|
|
517
|
+
|
|
518
|
+
# Ensure the undo stack top still represents the "pre-yank" snapshot.
|
|
519
|
+
if @yank_undo_snapshot
|
|
520
|
+
# If the most recent undo entry isn't our yank base, replace it.
|
|
521
|
+
if @undo_stack.empty? || @undo_stack.last != @yank_undo_snapshot
|
|
522
|
+
@undo_stack << @yank_undo_snapshot
|
|
523
|
+
@undo_stack.shift while @undo_stack.length > @undo_limit
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
clamp_mark!
|
|
527
|
+
@last_yank_len = y.length
|
|
528
|
+
@last_action = :yank_pop
|
|
529
|
+
y
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# :category: Undo and Redo helpers
|
|
533
|
+
|
|
534
|
+
def undo
|
|
535
|
+
return if @undo_stack.empty?
|
|
536
|
+
|
|
537
|
+
break_undo_chain!
|
|
538
|
+
@redo_stack << snapshot
|
|
539
|
+
restore(@undo_stack.pop)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def redo
|
|
543
|
+
return if @redo_stack.empty?
|
|
544
|
+
|
|
545
|
+
break_undo_chain!
|
|
546
|
+
@undo_stack << snapshot
|
|
547
|
+
restore(@redo_stack.pop)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# :category: Utilities
|
|
551
|
+
|
|
552
|
+
def display_width
|
|
553
|
+
Unicode::DisplayWidth.of(text)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Replace a from start, length characters; move cursor to end of insertion
|
|
557
|
+
def replace_span(start, length, str)
|
|
558
|
+
start = start.to_i
|
|
559
|
+
length = length.to_i
|
|
560
|
+
str = str.to_s
|
|
561
|
+
|
|
562
|
+
start = 0 if start.negative?
|
|
563
|
+
start = text.length if start > text.length
|
|
564
|
+
length = 0 if length.negative?
|
|
565
|
+
|
|
566
|
+
max_len = text.length - start
|
|
567
|
+
length = max_len if length > max_len
|
|
568
|
+
|
|
569
|
+
text.slice!(start, length) if length.positive?
|
|
570
|
+
text.insert(start, str) unless str.empty?
|
|
571
|
+
@cursor = start + str.length
|
|
572
|
+
adjust_mark_for_replace_span!(start, length, str.length)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Replace the text in the given Range with str; move cursor to end of
|
|
576
|
+
# insertion
|
|
577
|
+
def replace_range(range, str)
|
|
578
|
+
r = range
|
|
579
|
+
raise ArgumentError, "range required" unless r.is_a?(Range)
|
|
580
|
+
raise ArgumentError, "range must have begin and end" if r.begin.nil? || r.end.nil?
|
|
581
|
+
|
|
582
|
+
start = r.begin.to_i
|
|
583
|
+
finish = r.end.to_i
|
|
584
|
+
finish += 1 unless r.exclude_end?
|
|
585
|
+
|
|
586
|
+
replace_span(start, finish - start, str)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Replace the region with the str
|
|
590
|
+
def replace_region(str)
|
|
591
|
+
s = str.to_s
|
|
592
|
+
r = region_range
|
|
593
|
+
if r && r.begin < r.end
|
|
594
|
+
replace_span(r.begin, r.end - r.begin, s)
|
|
595
|
+
@mark = nil
|
|
596
|
+
else
|
|
597
|
+
text.insert(@cursor, s)
|
|
598
|
+
@cursor += s.length
|
|
599
|
+
end
|
|
600
|
+
s
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def delete_range(range)
|
|
604
|
+
r = clamp_range(range)
|
|
605
|
+
raise ArgumentError, "range required" unless r.is_a?(Range)
|
|
606
|
+
raise ArgumentError, "range must have begin and end" if r.begin.nil? || r.end.nil?
|
|
607
|
+
return "" if r.begin == r.end
|
|
608
|
+
|
|
609
|
+
deleted = text[r] || ""
|
|
610
|
+
replace_range(r, "")
|
|
611
|
+
deleted
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Return a Range that corresponds to the whole word that is "around" the
|
|
615
|
+
# cursor. The whole region if region active, the word cursor is on a if
|
|
616
|
+
# it's on a word, or nothing otherwise.
|
|
617
|
+
def word_at_point_range(from = cursor)
|
|
618
|
+
if region_active?
|
|
619
|
+
a, b = region_range.minmax
|
|
620
|
+
return a...b
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
chars = text.chars
|
|
624
|
+
return from...from if from < 0 || from > chars.length
|
|
625
|
+
return from...from if chars.empty?
|
|
626
|
+
|
|
627
|
+
on_word =
|
|
628
|
+
if from == chars.length
|
|
629
|
+
from.positive? && word_char?(chars[from - 1])
|
|
630
|
+
else
|
|
631
|
+
word_char?(chars[from])
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
return from...from unless on_word
|
|
635
|
+
|
|
636
|
+
left = from
|
|
637
|
+
if left == chars.length
|
|
638
|
+
left -= 1
|
|
639
|
+
end
|
|
640
|
+
left -= 1 while left.positive? && word_char?(chars[left - 1])
|
|
641
|
+
|
|
642
|
+
right = from
|
|
643
|
+
right += 1 while right < chars.length && word_char?(chars[right])
|
|
644
|
+
|
|
645
|
+
left...right
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Return the Range of the buffer that a completion should replace when the
|
|
649
|
+
# completion is accepted.
|
|
650
|
+
def completion_replace_range(from = cursor)
|
|
651
|
+
if region_active?
|
|
652
|
+
r = region_range
|
|
653
|
+
return r if r
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
prefix = completion_prefix(from)
|
|
657
|
+
chars = text.chars
|
|
658
|
+
at_word =
|
|
659
|
+
from < chars.length && word_char?(chars[from])
|
|
660
|
+
|
|
661
|
+
unless prefix.empty?
|
|
662
|
+
if at_word
|
|
663
|
+
return word_at_point_range(from)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
back = word_span_backward(from)
|
|
667
|
+
return back if back.begin < back.end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
if at_word
|
|
671
|
+
fwd = word_span_forward(from)
|
|
672
|
+
return fwd.end...fwd.end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
from...from
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def completion_prefix(from = cursor)
|
|
679
|
+
if region_active?
|
|
680
|
+
r = region_range
|
|
681
|
+
r ? text[r].to_s : ""
|
|
682
|
+
else
|
|
683
|
+
chars = text.chars
|
|
684
|
+
if from.positive? && !chars.empty?
|
|
685
|
+
if from < chars.length && word_char?(chars[from])
|
|
686
|
+
r = word_at_point_range(from)
|
|
687
|
+
r ? text[r.begin...from].to_s : ""
|
|
688
|
+
elsif word_char?(chars[from - 1])
|
|
689
|
+
r = word_span_backward(from)
|
|
690
|
+
r ? text[r].to_s : ""
|
|
691
|
+
else
|
|
692
|
+
""
|
|
693
|
+
end
|
|
694
|
+
else
|
|
695
|
+
""
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def accept_virtual_suffix!
|
|
701
|
+
return if @virtual_suffix.to_s.empty?
|
|
702
|
+
|
|
703
|
+
@text = virtual_text
|
|
704
|
+
@cursor = @text.length
|
|
705
|
+
@virtual_suffix = +""
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
private
|
|
709
|
+
|
|
710
|
+
def move_word_right_once
|
|
711
|
+
break_undo_chain!
|
|
712
|
+
chars = virtual_text.chars
|
|
713
|
+
i = @cursor
|
|
714
|
+
# skip non-word
|
|
715
|
+
i += 1 while i < chars.length && !word_char?(chars[i])
|
|
716
|
+
# skip word
|
|
717
|
+
i += 1 while i < chars.length && word_char?(chars[i])
|
|
718
|
+
@cursor = i
|
|
719
|
+
promote_to_cursor!
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def move_word_left_once
|
|
723
|
+
break_undo_chain!
|
|
724
|
+
chars = text.chars
|
|
725
|
+
i = @cursor
|
|
726
|
+
# skip non-word chars to the left
|
|
727
|
+
i -= 1 while i > 0 && !word_char?(chars[i - 1])
|
|
728
|
+
# skip word chars to the left
|
|
729
|
+
i -= 1 while i > 0 && word_char?(chars[i - 1])
|
|
730
|
+
@cursor = i
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def transpose_chars_once
|
|
734
|
+
i =
|
|
735
|
+
if @cursor == text.length
|
|
736
|
+
@cursor - 2
|
|
737
|
+
else
|
|
738
|
+
@cursor - 1
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
return if i < 0
|
|
742
|
+
return if i + 1 >= text.length
|
|
743
|
+
|
|
744
|
+
a = text[i]
|
|
745
|
+
b = text[i + 1]
|
|
746
|
+
text[i] = b
|
|
747
|
+
text[i + 1] = a
|
|
748
|
+
|
|
749
|
+
@cursor = [i + 2, text.length].min
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def transpose_words_once
|
|
753
|
+
left, right = transpose_word_ranges
|
|
754
|
+
return false unless left && right
|
|
755
|
+
return false if left.begin == left.end || right.begin == right.end
|
|
756
|
+
return false if left.end > right.begin
|
|
757
|
+
|
|
758
|
+
left_text = text[left]
|
|
759
|
+
middle = text[left.end...right.begin].to_s
|
|
760
|
+
right_text = text[right]
|
|
761
|
+
|
|
762
|
+
replace_span(left.begin, right.end - left.begin, right_text + middle + left_text)
|
|
763
|
+
@cursor = left.begin + right_text.length + middle.length + left_text.length
|
|
764
|
+
true
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def transpose_word_ranges
|
|
768
|
+
here = word_at_point_range(@cursor)
|
|
769
|
+
|
|
770
|
+
if here.begin < here.end
|
|
771
|
+
left = previous_word_range(here.begin)
|
|
772
|
+
right = next_word_range(here.end)
|
|
773
|
+
|
|
774
|
+
if right.begin < right.end
|
|
775
|
+
[here, right]
|
|
776
|
+
elsif left.begin < left.end
|
|
777
|
+
[left, here]
|
|
778
|
+
else
|
|
779
|
+
[nil, nil]
|
|
780
|
+
end
|
|
781
|
+
else
|
|
782
|
+
left = previous_word_range(@cursor)
|
|
783
|
+
right = next_word_range(@cursor)
|
|
784
|
+
|
|
785
|
+
if left.begin < left.end && right.begin < right.end
|
|
786
|
+
[left, right]
|
|
787
|
+
else
|
|
788
|
+
[nil, nil]
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def previous_word_range(from)
|
|
794
|
+
span = word_span_backward(from)
|
|
795
|
+
if span.begin < span.end
|
|
796
|
+
word_at_point_range(span.begin)
|
|
797
|
+
else
|
|
798
|
+
from...from
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def next_word_range(from)
|
|
803
|
+
chars = text.chars
|
|
804
|
+
i = from
|
|
805
|
+
i += 1 while i < chars.length && !word_char?(chars[i])
|
|
806
|
+
|
|
807
|
+
if i < chars.length && word_char?(chars[i])
|
|
808
|
+
word_at_point_range(i)
|
|
809
|
+
else
|
|
810
|
+
from...from
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def with_undo(before: nil)
|
|
815
|
+
break_undo_chain!
|
|
816
|
+
before ||= snapshot
|
|
817
|
+
yield
|
|
818
|
+
after = snapshot
|
|
819
|
+
|
|
820
|
+
if after != before
|
|
821
|
+
@undo_stack << before
|
|
822
|
+
@undo_stack.shift while @undo_stack.length > @undo_limit
|
|
823
|
+
@redo_stack.clear
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
clamp_mark!
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def promote_to_cursor!
|
|
830
|
+
real_len = @text.length
|
|
831
|
+
return if @cursor <= real_len
|
|
832
|
+
|
|
833
|
+
full = virtual_text
|
|
834
|
+
@cursor = full.length if @cursor > full.length
|
|
835
|
+
promoted = full[0, @cursor] || ""
|
|
836
|
+
remaining = full[@cursor..] || ""
|
|
837
|
+
|
|
838
|
+
@text = promoted
|
|
839
|
+
@virtual_suffix = remaining
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# This consolidates a number of actions of the same kind into a single
|
|
843
|
+
# undo step, especially insert benefits from this since you don't want
|
|
844
|
+
# each character typed to be a separate undo step.
|
|
845
|
+
def with_undo_coalesced(kind)
|
|
846
|
+
before = snapshot
|
|
847
|
+
chain = @undo_chain
|
|
848
|
+
start_new =
|
|
849
|
+
chain.nil? ||
|
|
850
|
+
chain[:kind] != kind ||
|
|
851
|
+
chain[:cursor0] != @cursor # cursor must be where the chain expects
|
|
852
|
+
|
|
853
|
+
if start_new
|
|
854
|
+
@undo_stack << before
|
|
855
|
+
@undo_stack.shift while @undo_stack.length > @undo_limit
|
|
856
|
+
@redo_stack.clear
|
|
857
|
+
@undo_chain = { kind: kind, cursor0: @cursor }
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
yield
|
|
861
|
+
|
|
862
|
+
# update expected cursor for continued typing
|
|
863
|
+
@undo_chain[:cursor0] = @cursor if @undo_chain
|
|
864
|
+
clamp_mark!
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def break_undo_chain!
|
|
868
|
+
@undo_chain = nil
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def snapshot
|
|
872
|
+
[@text.dup, @cursor]
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def restore(snap)
|
|
876
|
+
@text, @cursor = snap
|
|
877
|
+
clamp_cursor!
|
|
878
|
+
@text
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def normalize_count(count)
|
|
882
|
+
n =
|
|
883
|
+
begin
|
|
884
|
+
Integer(count)
|
|
885
|
+
rescue StandardError
|
|
886
|
+
1
|
|
887
|
+
end
|
|
888
|
+
n = 1 if n < 1
|
|
889
|
+
n
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def repeat(count = 1)
|
|
893
|
+
n = normalize_count(count)
|
|
894
|
+
result = nil
|
|
895
|
+
i = 0
|
|
896
|
+
while i < n
|
|
897
|
+
result = yield
|
|
898
|
+
i += 1
|
|
899
|
+
end
|
|
900
|
+
result
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def clamp_range(range)
|
|
904
|
+
raise ArgumentError, "range required" unless range.is_a?(Range)
|
|
905
|
+
|
|
906
|
+
len = text.length
|
|
907
|
+
s = range.begin.to_i
|
|
908
|
+
e = range.end.to_i
|
|
909
|
+
e += 1 unless range.exclude_end?
|
|
910
|
+
|
|
911
|
+
s = 0 if s < 0
|
|
912
|
+
s = len if s > len
|
|
913
|
+
e = 0 if e < 0
|
|
914
|
+
e = len if e > len
|
|
915
|
+
e = s if e < s
|
|
916
|
+
|
|
917
|
+
s...e
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def word_span_forward(from = cursor)
|
|
921
|
+
chars = text.chars
|
|
922
|
+
i = from
|
|
923
|
+
|
|
924
|
+
i += 1 while i < chars.length && !word_char?(chars[i])
|
|
925
|
+
i += 1 while i < chars.length && word_char?(chars[i])
|
|
926
|
+
|
|
927
|
+
from...i
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def word_span_backward(from = cursor)
|
|
931
|
+
chars = text.chars
|
|
932
|
+
i = from - 1
|
|
933
|
+
return from...from if i < 0
|
|
934
|
+
|
|
935
|
+
# skip non-word
|
|
936
|
+
i -= 1 while i > 0 && !word_char?(chars[i])
|
|
937
|
+
# skip word
|
|
938
|
+
i -= 1 while i > 0 && word_char?(chars[i])
|
|
939
|
+
|
|
940
|
+
start = i == 0 && word_char?(chars[i]) ? 0 : i + 1
|
|
941
|
+
start...from
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def clamp_cursor!
|
|
945
|
+
@cursor = 0 if @cursor.negative?
|
|
946
|
+
max = @text.length
|
|
947
|
+
@cursor = max if @cursor > max
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def clamp_mark!
|
|
951
|
+
if @mark
|
|
952
|
+
@mark = 0 if @mark.negative?
|
|
953
|
+
max = @text.length
|
|
954
|
+
@mark = max if @mark > max
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def adjust_mark_for_replace_span!(start, removed_len, inserted_len)
|
|
959
|
+
return unless @mark
|
|
960
|
+
|
|
961
|
+
s = start.to_i
|
|
962
|
+
rem = removed_len.to_i
|
|
963
|
+
ins = inserted_len.to_i
|
|
964
|
+
delta = ins - rem
|
|
965
|
+
|
|
966
|
+
if s <= @mark
|
|
967
|
+
if @mark < s + rem
|
|
968
|
+
# mark was inside removed region; snap to end of inserted region
|
|
969
|
+
@mark = s + ins
|
|
970
|
+
else
|
|
971
|
+
@mark += delta
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
clamp_mark!
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
def push_kill(str, append: true)
|
|
979
|
+
s = str.to_s
|
|
980
|
+
return if s.empty?
|
|
981
|
+
|
|
982
|
+
if @last_action == :kill && !@kill_ring.empty?
|
|
983
|
+
@kill_ring[0] = if append
|
|
984
|
+
@kill_ring[0] + s
|
|
985
|
+
else
|
|
986
|
+
s + @kill_ring[0]
|
|
987
|
+
end
|
|
988
|
+
else
|
|
989
|
+
@kill_ring.unshift(s)
|
|
990
|
+
end
|
|
991
|
+
@kill_ring.pop while @kill_ring.length > @kill_ring_max
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def word_char?(ch)
|
|
995
|
+
@word_re.match?(ch)
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
end
|