coolline 0.0.1pre
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.
- data/.gemtest +0 -0
- data/lib/coolline.rb +7 -0
- data/lib/coolline/coolline.rb +369 -0
- data/lib/coolline/editor.rb +227 -0
- data/lib/coolline/handler.rb +17 -0
- data/lib/coolline/history.rb +71 -0
- data/lib/coolline/version.rb +3 -0
- data/repl.rb +28 -0
- data/test/editor_test.rb +727 -0
- data/test/helpers.rb +12 -0
- data/test/history_test.rb +85 -0
- data/test/run_all.rb +5 -0
- metadata +71 -0
data/.gemtest
ADDED
File without changes
|
data/lib/coolline.rb
ADDED
@@ -0,0 +1,369 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
class Coolline
|
4
|
+
if ENV["XDG_CONFIG_HOME"]
|
5
|
+
ConfigDir = ENV["XDG_CONFIG_HOME"]
|
6
|
+
ConfigFile = File.join(ConfigDir, "coolline.rb")
|
7
|
+
else
|
8
|
+
ConfigDir = ENV["HOME"]
|
9
|
+
ConfigFile = File.join(ConfigDir, ".coolline.rb")
|
10
|
+
end
|
11
|
+
|
12
|
+
HistoryFile = File.join(ConfigDir, ".coolline-history")
|
13
|
+
|
14
|
+
NullFile = "/dev/null"
|
15
|
+
|
16
|
+
AnsiCode = %r{(\e\[\??\d+(?:;\d+)?\w)}
|
17
|
+
|
18
|
+
# @return [Hash] All the defaults settings
|
19
|
+
Settings = {
|
20
|
+
:word_boundaries => [" ", "-", "_"],
|
21
|
+
|
22
|
+
:handlers =>
|
23
|
+
[
|
24
|
+
Handler.new(/\A(?:\C-h|\x7F)\z/, &:kill_backward_char),
|
25
|
+
Handler.new("\C-a", &:beginning_of_line),
|
26
|
+
Handler.new("\C-e", &:end_of_line),
|
27
|
+
Handler.new("\C-k", &:kill_line),
|
28
|
+
Handler.new("\C-f", &:forward_char),
|
29
|
+
Handler.new("\C-b", &:backward_char),
|
30
|
+
Handler.new("\C-d", &:kill_current_char),
|
31
|
+
Handler.new("\C-c") { Process.kill(:INT, Process.pid) },
|
32
|
+
Handler.new("\C-w", &:kill_backward_word),
|
33
|
+
Handler.new("\C-t", &:transpose_chars),
|
34
|
+
Handler.new("\C-n", &:next_history_line),
|
35
|
+
Handler.new("\C-p", &:previous_history_line),
|
36
|
+
Handler.new("\C-r", &:interactive_search),
|
37
|
+
Handler.new("\t", &:complete),
|
38
|
+
Handler.new("\C-a".."\C-z") {},
|
39
|
+
|
40
|
+
Handler.new(/\A\e(?:\C-h|\x7F)\z/, &:kill_backward_word),
|
41
|
+
Handler.new("\eb", &:backward_word),
|
42
|
+
Handler.new("\ef", &:forward_word),
|
43
|
+
Handler.new("\e[A", &:previous_history_line),
|
44
|
+
Handler.new("\e[B", &:next_history_line),
|
45
|
+
Handler.new("\e[5~", &:previous_history_line),
|
46
|
+
Handler.new("\e[6~", &:next_history_line),
|
47
|
+
Handler.new("\e[C", &:forward_char),
|
48
|
+
Handler.new("\e[D", &:backward_char),
|
49
|
+
Handler.new("\et", &:transpose_words),
|
50
|
+
Handler.new("\ec", &:capitalize_word),
|
51
|
+
Handler.new("\eu", &:uppercase_word),
|
52
|
+
Handler.new("\el", &:lowercase_word),
|
53
|
+
|
54
|
+
Handler.new(/\e.+/) {},
|
55
|
+
],
|
56
|
+
|
57
|
+
:unknown_char_proc => :insert_string.to_proc,
|
58
|
+
:transform_proc => :line.to_proc,
|
59
|
+
:completion_proc => proc { |cool| [] },
|
60
|
+
|
61
|
+
:history_file => HistoryFile,
|
62
|
+
:history_size => 5000,
|
63
|
+
}
|
64
|
+
|
65
|
+
include Coolline::Editor
|
66
|
+
|
67
|
+
@config_loaded = false
|
68
|
+
|
69
|
+
# Loads the config, even if it has already been loaded
|
70
|
+
def self.load_config!
|
71
|
+
if File.exist? ConfigFile
|
72
|
+
load ConfigFile
|
73
|
+
end
|
74
|
+
|
75
|
+
@config_loaded = true
|
76
|
+
end
|
77
|
+
|
78
|
+
# Loads the config, unless it has already been loaded
|
79
|
+
def self.load_config
|
80
|
+
load_config! unless @config_loaded
|
81
|
+
end
|
82
|
+
|
83
|
+
# Creates a new cool line.
|
84
|
+
#
|
85
|
+
# @yieldparam [Coolline] self
|
86
|
+
def initialize
|
87
|
+
self.class.load_config
|
88
|
+
|
89
|
+
@input = STDIN # must be the actual IO object
|
90
|
+
@output = $stdout
|
91
|
+
|
92
|
+
self.word_boundaries = Settings[:word_boundaries].dup
|
93
|
+
self.handlers = Settings[:handlers].dup
|
94
|
+
self.transform_proc = Settings[:transform_proc]
|
95
|
+
self.unknown_char_proc = Settings[:unknown_char_proc]
|
96
|
+
self.completion_proc = Settings[:completion_proc]
|
97
|
+
self.history_file = Settings[:history_file]
|
98
|
+
self.history_size = Settings[:history_size]
|
99
|
+
|
100
|
+
yield self if block_given?
|
101
|
+
|
102
|
+
@history ||= History.new(@history_file, @history_size)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [IO]
|
106
|
+
attr_accessor :input, :output
|
107
|
+
|
108
|
+
# @return [Array<String, Regexp>] Expressions detected as word boundaries
|
109
|
+
attr_reader :word_boundaries
|
110
|
+
|
111
|
+
# @return [Regexp] Regular expression to match word boundaries
|
112
|
+
attr_reader :word_boundaries_regexp
|
113
|
+
|
114
|
+
def word_boundaries=(array)
|
115
|
+
@word_boundaries = array
|
116
|
+
@word_boundaries_regexp = /\A#{Regexp.union(*array)}\z/
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Proc] Proc called to change the way a line is displayed
|
120
|
+
attr_accessor :transform_proc
|
121
|
+
|
122
|
+
# @return [Proc] Proc called to handle unmatched characters
|
123
|
+
attr_accessor :unknown_char_proc
|
124
|
+
|
125
|
+
# @return [Proc] Proc called to retrieve completions
|
126
|
+
attr_accessor :completion_proc
|
127
|
+
|
128
|
+
# @return [Array<Handler>]
|
129
|
+
attr_accessor :handlers
|
130
|
+
|
131
|
+
# @return [String] Name of the file containing history
|
132
|
+
attr_accessor :history_file
|
133
|
+
|
134
|
+
# @return [Integer] Size of the history
|
135
|
+
attr_accessor :history_size
|
136
|
+
|
137
|
+
# @return [History] History object
|
138
|
+
attr_accessor :history
|
139
|
+
|
140
|
+
# @return [String] Current line
|
141
|
+
attr_reader :line
|
142
|
+
|
143
|
+
# @return [Integer] Cursor position
|
144
|
+
attr_accessor :pos
|
145
|
+
|
146
|
+
# @return [String] Current prompt
|
147
|
+
attr_accessor :prompt
|
148
|
+
|
149
|
+
# Reads a line from the terminal
|
150
|
+
# @param [String] prompt Characters to print before each line
|
151
|
+
def readline(prompt = ">> ")
|
152
|
+
@prompt = prompt
|
153
|
+
|
154
|
+
@line = ""
|
155
|
+
@pos = 0
|
156
|
+
@accumulator = nil
|
157
|
+
|
158
|
+
@history_index = @history.size
|
159
|
+
@history_moved = false
|
160
|
+
|
161
|
+
print "\r\e[0m\e[0K"
|
162
|
+
print @prompt
|
163
|
+
|
164
|
+
until (char = @input.getch) == "\r"
|
165
|
+
handle(char)
|
166
|
+
|
167
|
+
if @history_moved
|
168
|
+
@history_moved = false
|
169
|
+
else
|
170
|
+
@history_index = @history.size
|
171
|
+
end
|
172
|
+
|
173
|
+
width = @input.winsize[1]
|
174
|
+
prompt_size = strip_ansi_codes(@prompt).size
|
175
|
+
line = transform(@line)
|
176
|
+
|
177
|
+
stripped_line_width = strip_ansi_codes(line).size
|
178
|
+
line << " " * [width - stripped_line_width - prompt_size, 0].max
|
179
|
+
|
180
|
+
# reset the color, and kill the line
|
181
|
+
print "\r\e[0m\e[0K"
|
182
|
+
|
183
|
+
if strip_ansi_codes(@prompt + line).size <= width
|
184
|
+
print @prompt + line
|
185
|
+
print "\e[#{prompt_size + @pos + 1}G"
|
186
|
+
else
|
187
|
+
print @prompt
|
188
|
+
|
189
|
+
left_width = width - prompt_size
|
190
|
+
|
191
|
+
start_index = [@pos - left_width + 1, 0].max
|
192
|
+
end_index = start_index + left_width - 1
|
193
|
+
|
194
|
+
i = 0
|
195
|
+
line.split(AnsiCode).each do |str|
|
196
|
+
if start_with_ansi_code? str
|
197
|
+
# always print ansi codes to ensure the color is right
|
198
|
+
print str
|
199
|
+
else
|
200
|
+
if i >= start_index
|
201
|
+
print str[0..(end_index - i)]
|
202
|
+
elsif i < start_index && i + str.size >= start_index
|
203
|
+
print str[(start_index - i), left_width]
|
204
|
+
end
|
205
|
+
|
206
|
+
i += str.size
|
207
|
+
break if i >= end_index
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
if @pos < left_width + 1
|
212
|
+
print "\e[#{prompt_size + @pos + 1}G"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
print "\n"
|
218
|
+
|
219
|
+
@history << @line
|
220
|
+
|
221
|
+
@line + "\n"
|
222
|
+
end
|
223
|
+
|
224
|
+
# Reads a line with no prompt
|
225
|
+
def gets
|
226
|
+
readline ""
|
227
|
+
end
|
228
|
+
|
229
|
+
# Prints objects to the output.
|
230
|
+
def print(*objs)
|
231
|
+
@output.print(*objs)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Selects the previous line in history (if any)
|
235
|
+
def previous_history_line
|
236
|
+
if @history_index - 1 >= 0
|
237
|
+
@line.replace @history[@history_index - 1]
|
238
|
+
@pos = [@line.size, @pos].min
|
239
|
+
|
240
|
+
@history_index -= 1
|
241
|
+
end
|
242
|
+
|
243
|
+
@history_moved = true
|
244
|
+
end
|
245
|
+
|
246
|
+
# Selects the next line in history (if any).
|
247
|
+
#
|
248
|
+
# When on the last line, this method replaces the current line with an empty
|
249
|
+
# string.
|
250
|
+
def next_history_line
|
251
|
+
if @history_index + 1 <= @history.size
|
252
|
+
@line.replace @history[@history_index + 1] || ""
|
253
|
+
@pos = [@line.size, @pos].min
|
254
|
+
|
255
|
+
@history_index += 1
|
256
|
+
end
|
257
|
+
|
258
|
+
@history_moved = true
|
259
|
+
end
|
260
|
+
|
261
|
+
# Prompts the user to search for a line
|
262
|
+
def interactive_search
|
263
|
+
initial_index = @history_index
|
264
|
+
found_index = @history_index
|
265
|
+
|
266
|
+
# Use another coolline instance for the search! :D
|
267
|
+
Coolline.new { |c|
|
268
|
+
# Remove the search handler (to avoid nesting confusion)
|
269
|
+
c.handlers.delete_if { |h| h.char == "\C-r" }
|
270
|
+
|
271
|
+
# search line
|
272
|
+
c.transform_proc = proc do
|
273
|
+
pattern = Regexp.new Regexp.escape(c.line)
|
274
|
+
|
275
|
+
line, found_index = @history.search(pattern, @history_index).first
|
276
|
+
|
277
|
+
if line
|
278
|
+
"#{c.line}): #{line}"
|
279
|
+
else
|
280
|
+
"#{c.line}): [pattern not found]"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Disable history
|
285
|
+
c.history_file = NullFile
|
286
|
+
c.history_size = 0
|
287
|
+
}.readline("(search:")
|
288
|
+
|
289
|
+
@line.replace @history[found_index]
|
290
|
+
@pos = [@line.size, @pos].min
|
291
|
+
|
292
|
+
@history_index = found_index
|
293
|
+
@history_moved = true
|
294
|
+
end
|
295
|
+
|
296
|
+
# @return [String] The string to be completed (useful in the completion proc)
|
297
|
+
def completed_word
|
298
|
+
line[word_beginning_before(pos)...pos]
|
299
|
+
end
|
300
|
+
|
301
|
+
# Tries to complete the current word
|
302
|
+
def complete
|
303
|
+
return if word_boundary? line[pos - 1]
|
304
|
+
|
305
|
+
completions = @completion_proc.call(self)
|
306
|
+
return if completions.empty?
|
307
|
+
|
308
|
+
result = completions.inject do |common, el|
|
309
|
+
i = 0
|
310
|
+
i += 1 while common[i] == el[i]
|
311
|
+
|
312
|
+
el[0...i]
|
313
|
+
end
|
314
|
+
|
315
|
+
beg = word_beginning_before(pos)
|
316
|
+
line[beg...pos] = result
|
317
|
+
self.pos = beg + result.size
|
318
|
+
end
|
319
|
+
|
320
|
+
def word_boundary?(char)
|
321
|
+
char =~ word_boundaries_regexp
|
322
|
+
end
|
323
|
+
|
324
|
+
def strip_ansi_codes(string)
|
325
|
+
string.gsub(AnsiCode, "")
|
326
|
+
end
|
327
|
+
|
328
|
+
def start_with_ansi_code?(string)
|
329
|
+
(string =~ AnsiCode) == 0
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
def transform(line)
|
334
|
+
@transform_proc.call(line)
|
335
|
+
end
|
336
|
+
|
337
|
+
def handle(char)
|
338
|
+
input = if @accumulator
|
339
|
+
handle_escape(char)
|
340
|
+
elsif char == "\e"
|
341
|
+
@accumulator = "\e"
|
342
|
+
nil
|
343
|
+
else
|
344
|
+
char
|
345
|
+
end
|
346
|
+
|
347
|
+
if input
|
348
|
+
if handler = @handlers.find { |h| h === input }
|
349
|
+
handler.call self
|
350
|
+
else
|
351
|
+
@unknown_char_proc.call self, char
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def handle_escape(char)
|
357
|
+
if char == "[" && @accumulator =~ /\A\e?\e\z/ or
|
358
|
+
char =~ /\d/ && @accumulator =~ /\A\e?\e\[\d*\z/ or
|
359
|
+
char == "\e" && @accumulator == "\e"
|
360
|
+
@accumulator << char
|
361
|
+
nil
|
362
|
+
else
|
363
|
+
str = @accumulator + char
|
364
|
+
@accumulator = nil
|
365
|
+
|
366
|
+
str
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
class Coolline
|
2
|
+
module Editor
|
3
|
+
# Inserts a string at the current point in the line
|
4
|
+
#
|
5
|
+
# @param [String] string String to be inserted
|
6
|
+
def insert_string(string)
|
7
|
+
line.insert pos, string
|
8
|
+
self.pos += string.size
|
9
|
+
end
|
10
|
+
|
11
|
+
def word_boundary_after(pos)
|
12
|
+
pos += 1 # don't return initial pos
|
13
|
+
pos += 1 until pos >= line.size or word_boundary? line[pos]
|
14
|
+
pos >= line.size ? nil : pos
|
15
|
+
end
|
16
|
+
|
17
|
+
def word_boundary_before(pos)
|
18
|
+
pos -= 1
|
19
|
+
pos -= 1 until pos < 0 or word_boundary? line[pos]
|
20
|
+
pos < 0 ? nil : pos
|
21
|
+
end
|
22
|
+
|
23
|
+
def non_word_boundary_after(pos)
|
24
|
+
pos += 1
|
25
|
+
pos += 1 while pos < line.size and word_boundary? line[pos]
|
26
|
+
pos >= line.size ? nil : pos
|
27
|
+
end
|
28
|
+
|
29
|
+
def non_word_boundary_before(pos)
|
30
|
+
pos -= 1
|
31
|
+
pos -= 1 while pos >= 0 and word_boundary? line[pos]
|
32
|
+
pos < 0 ? nil : pos
|
33
|
+
end
|
34
|
+
|
35
|
+
def word_end_before(pos)
|
36
|
+
pos -= 1
|
37
|
+
|
38
|
+
if line[pos] and word_boundary? line[pos]
|
39
|
+
non_word_boundary_before(pos) || 0
|
40
|
+
else
|
41
|
+
pos
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def word_beginning_before(pos)
|
46
|
+
word_end = word_end_before(pos)
|
47
|
+
|
48
|
+
if first = word_boundary_before(word_end)
|
49
|
+
first + 1
|
50
|
+
else
|
51
|
+
0
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def word_beginning_after(pos)
|
56
|
+
if line[pos] and word_boundary? line[pos]
|
57
|
+
non_word_boundary_after(pos) || line.size
|
58
|
+
else
|
59
|
+
pos
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def word_end_after(pos)
|
64
|
+
word_beg = word_beginning_after(pos)
|
65
|
+
|
66
|
+
if first = word_boundary_after(word_beg)
|
67
|
+
first - 1
|
68
|
+
else
|
69
|
+
line.size - 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Moves the cursor to the beginning of the line
|
74
|
+
def beginning_of_line
|
75
|
+
self.pos = 0
|
76
|
+
end
|
77
|
+
|
78
|
+
# Moves to the end of the line
|
79
|
+
def end_of_line
|
80
|
+
self.pos = line.size
|
81
|
+
end
|
82
|
+
|
83
|
+
# Moves to the previous character
|
84
|
+
def backward_char
|
85
|
+
self.pos -= 1 if pos != 0
|
86
|
+
end
|
87
|
+
|
88
|
+
# Moves to the next character
|
89
|
+
def forward_char
|
90
|
+
self.pos += 1 if pos != line.size
|
91
|
+
end
|
92
|
+
|
93
|
+
# Moves to the previous word
|
94
|
+
def backward_word
|
95
|
+
self.pos = word_beginning_before(pos) if pos > 0
|
96
|
+
end
|
97
|
+
|
98
|
+
# Moves to the next word
|
99
|
+
def forward_word
|
100
|
+
self.pos = word_end_after(pos) + 1 if pos != line.size
|
101
|
+
end
|
102
|
+
|
103
|
+
# Removes the previous word
|
104
|
+
def kill_backward_word
|
105
|
+
if pos > 0
|
106
|
+
beg = word_beginning_before(pos)
|
107
|
+
|
108
|
+
line[beg...pos] = ""
|
109
|
+
self.pos = beg
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Removes the previous character
|
114
|
+
def kill_backward_char
|
115
|
+
if pos > 0
|
116
|
+
line[pos - 1] = ""
|
117
|
+
self.pos -= 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Removes the current character
|
122
|
+
def kill_current_char
|
123
|
+
line[pos] = "" if pos != line.size
|
124
|
+
end
|
125
|
+
|
126
|
+
# Removes all the characters beyond the current point
|
127
|
+
def kill_line
|
128
|
+
line[pos..-1] = ""
|
129
|
+
end
|
130
|
+
|
131
|
+
# Swaps the previous character with the current one
|
132
|
+
def transpose_chars
|
133
|
+
if line.size >= 2
|
134
|
+
if pos == line.size
|
135
|
+
line[pos - 2], line[pos - 1] = line[pos - 1], line[pos - 2]
|
136
|
+
else
|
137
|
+
line[pos - 1], line[pos] = line[pos], line[pos - 1]
|
138
|
+
self.pos += 1
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Swaps the current word with the previous word, or the two previous words
|
144
|
+
# if we're at the end of the line.
|
145
|
+
def transpose_words
|
146
|
+
if !non_word_boundary_after(pos)
|
147
|
+
last = non_word_boundary_before(pos)
|
148
|
+
return unless last
|
149
|
+
|
150
|
+
last_beg = word_beginning_before(last)
|
151
|
+
return unless word_boundary_before(last_beg)
|
152
|
+
|
153
|
+
whitespace_beg = word_end_before(last_beg)
|
154
|
+
return unless non_word_boundary_before(whitespace_beg + 1)
|
155
|
+
|
156
|
+
first_beg = word_beginning_before(last_beg)
|
157
|
+
|
158
|
+
line[first_beg..last] = line[last_beg..last] +
|
159
|
+
line[(whitespace_beg + 1)...last_beg] +
|
160
|
+
line[first_beg..whitespace_beg]
|
161
|
+
|
162
|
+
self.pos = last + 1
|
163
|
+
elsif word_boundary? line[pos] # between two words?
|
164
|
+
return unless non_word_boundary_before(pos)
|
165
|
+
|
166
|
+
last_beg = word_beginning_after(pos)
|
167
|
+
last_end = word_end_after(pos)
|
168
|
+
|
169
|
+
first_end = word_end_before(pos)
|
170
|
+
first_beg = word_beginning_before(pos)
|
171
|
+
|
172
|
+
line[first_beg..last_end] = line[last_beg..last_end] +
|
173
|
+
line[(first_end + 1)...last_beg] +
|
174
|
+
line[first_beg..first_end]
|
175
|
+
self.pos = last_end + 1
|
176
|
+
else # within a word?
|
177
|
+
return unless non_word_boundary_before(pos)
|
178
|
+
return unless word_boundary_before(pos)
|
179
|
+
|
180
|
+
last_beg = word_beginning_after(pos - 1)
|
181
|
+
last_end = word_end_after(pos)
|
182
|
+
|
183
|
+
return unless non_word_boundary_before(last_beg)
|
184
|
+
|
185
|
+
first_end = word_end_before(last_beg)
|
186
|
+
first_beg = word_beginning_before(last_beg)
|
187
|
+
|
188
|
+
line[first_beg..last_end] = line[last_beg..last_end] +
|
189
|
+
line[(first_end + 1)...last_beg] +
|
190
|
+
line[first_beg..first_end]
|
191
|
+
self.pos = last_end + 1
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Capitalizes the current word
|
196
|
+
def capitalize_word
|
197
|
+
if beg = word_beginning_after(pos) and last = word_end_after(pos)
|
198
|
+
line[beg..last] = line[beg..last].capitalize
|
199
|
+
self.pos = last + 1
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Lowercases the current word
|
204
|
+
def lowercase_word
|
205
|
+
if beg = word_beginning_after(pos) and last = word_end_after(pos)
|
206
|
+
line[beg..last] = line[beg..last].downcase
|
207
|
+
self.pos = last + 1
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Uppercases the current word
|
212
|
+
def uppercase_word
|
213
|
+
if beg = word_beginning_after(pos) and last = word_end_after(pos)
|
214
|
+
line[beg..last] = line[beg..last].upcase
|
215
|
+
self.pos = last + 1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# This method can be overriden to change characters considered as word
|
220
|
+
# boundaries.
|
221
|
+
#
|
222
|
+
# @return [Boolean] True if the string is a word boundary, false otherwise
|
223
|
+
def word_boundary?(string)
|
224
|
+
string =~ /\A[ \t]+\z/
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|