textbringer 13 → 15
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 +4 -4
- data/CLAUDE.md +197 -0
- data/lib/textbringer/buffer.rb +28 -204
- data/lib/textbringer/commands/buffers.rb +10 -31
- data/lib/textbringer/commands/isearch.rb +9 -3
- data/lib/textbringer/commands/ispell.rb +236 -0
- data/lib/textbringer/commands/misc.rb +4 -7
- data/lib/textbringer/commands/rectangle.rb +290 -0
- data/lib/textbringer/commands/replace.rb +8 -2
- data/lib/textbringer/config.rb +1 -0
- data/lib/textbringer/global_minor_mode.rb +58 -0
- data/lib/textbringer/keymap.rb +2 -0
- data/lib/textbringer/modes/completion_list_mode.rb +1 -4
- data/lib/textbringer/modes/transient_mark_mode.rb +101 -0
- data/lib/textbringer/utils.rb +8 -6
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer.rb +4 -0
- metadata +7 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Textbringer
|
|
7
|
+
module Commands
|
|
8
|
+
class Ispell
|
|
9
|
+
def initialize
|
|
10
|
+
@personal_dictionary_modified = false
|
|
11
|
+
@stdin, @stdout, @stderr, @wait_thr =
|
|
12
|
+
Open3.popen3(CONFIG[:ispell_command])
|
|
13
|
+
@stdout.gets # consume the banner
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check_word(word)
|
|
17
|
+
send_command("^" + word)
|
|
18
|
+
result = @stdout.gets
|
|
19
|
+
if result.nil? || result == "\n"
|
|
20
|
+
# aspell can't handle word, which may contain multibyte characters
|
|
21
|
+
return [word, nil]
|
|
22
|
+
end
|
|
23
|
+
@stdout.gets
|
|
24
|
+
case result
|
|
25
|
+
when /\A&\s+([^\s]+)\s+\d+\s+\d+:\s+(.*)/
|
|
26
|
+
[$1, $2.split(/, /)]
|
|
27
|
+
when /\A\*/, /\A\+/, /\A\-/, /\A\%/
|
|
28
|
+
[word, []]
|
|
29
|
+
when /\A#/
|
|
30
|
+
[word, nil]
|
|
31
|
+
else
|
|
32
|
+
raise "unexpected output from aspell: #{result}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_to_session_dictionary(word)
|
|
37
|
+
send_command("@" + word)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_to_personal_dictionary(word)
|
|
41
|
+
send_command("*" + word)
|
|
42
|
+
@personal_dictionary_modified = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def personal_dictionary_modified?
|
|
46
|
+
@personal_dictionary_modified
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def save_personal_dictionary
|
|
50
|
+
send_command("#")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def send_command(line)
|
|
54
|
+
@stdin.puts(line)
|
|
55
|
+
@stdin.flush
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def close
|
|
59
|
+
@stdin.close
|
|
60
|
+
@stdout.close
|
|
61
|
+
@stderr.close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_keymap :ISPELL_MODE_MAP
|
|
66
|
+
(?\x20..?\x7e).each do |c|
|
|
67
|
+
ISPELL_MODE_MAP.define_key(c, :ispell_unknown_command)
|
|
68
|
+
end
|
|
69
|
+
ISPELL_MODE_MAP.define_key(?\t, :ispell_unknown_command)
|
|
70
|
+
ISPELL_MODE_MAP.handle_undefined_key do |key|
|
|
71
|
+
ispell_unknown_command
|
|
72
|
+
end
|
|
73
|
+
ISPELL_MODE_MAP.define_key(?r, :ispell_replace)
|
|
74
|
+
ISPELL_MODE_MAP.define_key(?a, :ispell_accept)
|
|
75
|
+
ISPELL_MODE_MAP.define_key(?i, :ispell_insert)
|
|
76
|
+
ISPELL_MODE_MAP.define_key(" ", :ispell_skip)
|
|
77
|
+
ISPELL_MODE_MAP.define_key(?q, :ispell_quit)
|
|
78
|
+
ISPELL_MODE_MAP.define_key("\C-g", :ispell_quit)
|
|
79
|
+
|
|
80
|
+
ISPELL_STATUS = {}
|
|
81
|
+
|
|
82
|
+
URI_REGEXP = URI::RFC2396_PARSER.make_regexp(["http", "https", "ftp", "mailto"])
|
|
83
|
+
EMAIL_REGEXP = /
|
|
84
|
+
# local-part
|
|
85
|
+
( # dot-atom
|
|
86
|
+
(?<atom>[0-9a-z!\#$%&'*+\-\/=?^_`{|}~]+)
|
|
87
|
+
(\.\g<atom>)*
|
|
88
|
+
| # quoted-string
|
|
89
|
+
\"([\x20\x21\x23-\x5b\x5d-\x7e]
|
|
90
|
+
|\\[\x20-\x7e])*\"
|
|
91
|
+
)@
|
|
92
|
+
# domain
|
|
93
|
+
(?<sub_domain>[0-9a-z]([0-9a-z-]*[0-9a-z])?)
|
|
94
|
+
(\.\g<sub_domain>)*
|
|
95
|
+
/ix
|
|
96
|
+
ISPELL_WORD_REGEXP = /
|
|
97
|
+
(?<uri>#{URI_REGEXP})
|
|
98
|
+
| (?<email>#{EMAIL_REGEXP})
|
|
99
|
+
| (?<word>[[:alpha:]]+(?:'[[:alpha:]]+)*)
|
|
100
|
+
/x
|
|
101
|
+
|
|
102
|
+
define_command(:ispell_buffer) do |recursive_edit: false|
|
|
103
|
+
ISPELL_STATUS[:recursive_edit] = false
|
|
104
|
+
Buffer.current.beginning_of_buffer
|
|
105
|
+
ispell_mode
|
|
106
|
+
if !ispell_forward
|
|
107
|
+
ISPELL_STATUS[:recursive_edit] = recursive_edit
|
|
108
|
+
if recursive_edit
|
|
109
|
+
recursive_edit()
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ispell_done
|
|
115
|
+
# Don't delete visible_mark if mark is active (transient mark mode)
|
|
116
|
+
unless Buffer.current.mark_active?
|
|
117
|
+
Buffer.current.delete_visible_mark
|
|
118
|
+
end
|
|
119
|
+
Controller.current.overriding_map = nil
|
|
120
|
+
ISPELL_STATUS[:ispell]&.close
|
|
121
|
+
ISPELL_STATUS[:ispell] = nil
|
|
122
|
+
if ISPELL_STATUS[:recursive_edit]
|
|
123
|
+
exit_recursive_edit
|
|
124
|
+
end
|
|
125
|
+
ISPELL_STATUS[:recursive_edit] = false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ispell_mode
|
|
129
|
+
ISPELL_STATUS[:ispell] = Ispell.new
|
|
130
|
+
Controller.current.overriding_map = ISPELL_MODE_MAP
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ispell_forward
|
|
134
|
+
buffer = Buffer.current
|
|
135
|
+
while buffer.re_search_forward(ISPELL_WORD_REGEXP, raise_error: false,
|
|
136
|
+
goto_beginning: true)
|
|
137
|
+
if buffer.last_match[:word].nil?
|
|
138
|
+
buffer.goto_char(buffer.match_end(0))
|
|
139
|
+
next
|
|
140
|
+
end
|
|
141
|
+
ispell_beginning = buffer.point
|
|
142
|
+
# Don't update visible_mark if mark is already active (transient mark mode)
|
|
143
|
+
unless buffer.mark_active?
|
|
144
|
+
buffer.set_visible_mark
|
|
145
|
+
end
|
|
146
|
+
buffer.goto_char(buffer.match_end(0))
|
|
147
|
+
word = buffer.match_string(0)
|
|
148
|
+
_original, suggestions = ISPELL_STATUS[:ispell].check_word(word)
|
|
149
|
+
if !suggestions.nil? && !suggestions.empty?
|
|
150
|
+
ISPELL_STATUS[:beginning] = ispell_beginning
|
|
151
|
+
ISPELL_STATUS[:word] = word
|
|
152
|
+
ISPELL_STATUS[:suggestions] = suggestions
|
|
153
|
+
message_misspelled
|
|
154
|
+
recenter
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
Controller.current.overriding_map = nil
|
|
159
|
+
if ISPELL_STATUS[:ispell]&.personal_dictionary_modified? &&
|
|
160
|
+
y_or_n?("Personal dictionary modified. Save?")
|
|
161
|
+
ISPELL_STATUS[:ispell].save_personal_dictionary
|
|
162
|
+
end
|
|
163
|
+
message("Finished spelling check.")
|
|
164
|
+
ispell_done
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
define_command(:ispell_replace) do
|
|
169
|
+
ensure_ispell_active
|
|
170
|
+
word = ISPELL_STATUS[:word]
|
|
171
|
+
suggestions = ISPELL_STATUS[:suggestions]
|
|
172
|
+
Controller.current.overriding_map = nil
|
|
173
|
+
begin
|
|
174
|
+
s = read_from_minibuffer("Correct #{word} with: ",
|
|
175
|
+
completion_proc: ->(s) {
|
|
176
|
+
suggestions.grep(/^#{Regexp.quote(s)}/)
|
|
177
|
+
})
|
|
178
|
+
rescue Quit
|
|
179
|
+
message_misspelled
|
|
180
|
+
return
|
|
181
|
+
ensure
|
|
182
|
+
Controller.current.overriding_map = ISPELL_MODE_MAP
|
|
183
|
+
end
|
|
184
|
+
if !s.empty?
|
|
185
|
+
buffer = Buffer.current
|
|
186
|
+
pos = buffer.point
|
|
187
|
+
buffer.goto_char(ISPELL_STATUS[:beginning])
|
|
188
|
+
buffer.composite_edit do
|
|
189
|
+
buffer.delete_region(buffer.point, pos)
|
|
190
|
+
buffer.insert(s)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
ispell_forward
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
define_command(:ispell_accept) do
|
|
197
|
+
ensure_ispell_active
|
|
198
|
+
ISPELL_STATUS[:ispell].add_to_session_dictionary(ISPELL_STATUS[:word])
|
|
199
|
+
ispell_forward
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
define_command(:ispell_insert) do
|
|
203
|
+
ensure_ispell_active
|
|
204
|
+
ISPELL_STATUS[:ispell].add_to_personal_dictionary(ISPELL_STATUS[:word])
|
|
205
|
+
ispell_forward
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
define_command(:ispell_skip) do
|
|
209
|
+
ensure_ispell_active
|
|
210
|
+
ispell_forward
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
define_command(:ispell_quit) do
|
|
214
|
+
ensure_ispell_active
|
|
215
|
+
message("Quitting spell check.")
|
|
216
|
+
ispell_done
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
define_command(:ispell_unknown_command) do
|
|
220
|
+
ensure_ispell_active
|
|
221
|
+
message_misspelled
|
|
222
|
+
Window.beep
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def message_misspelled
|
|
226
|
+
word = ISPELL_STATUS[:word]
|
|
227
|
+
message("Misspelled: #{word} [r]eplace, [a]ccept, [i]nsert, [SPC] to skip, [q]uit")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def ensure_ispell_active
|
|
231
|
+
if ISPELL_STATUS[:ispell].nil?
|
|
232
|
+
raise EditorError, "ispell is not active"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -82,10 +82,9 @@ module Textbringer
|
|
|
82
82
|
|
|
83
83
|
def update_completions(xs)
|
|
84
84
|
if xs.size > 1
|
|
85
|
-
if COMPLETION[:
|
|
85
|
+
if COMPLETION[:completions_window].nil?
|
|
86
|
+
Window.list.last.split
|
|
86
87
|
COMPLETION[:completions_window] = Window.list.last
|
|
87
|
-
COMPLETION[:original_buffer] =
|
|
88
|
-
COMPLETION[:completions_window].buffer
|
|
89
88
|
end
|
|
90
89
|
completions = Buffer.find_or_new("*Completions*", undo_limit: 0)
|
|
91
90
|
if !completions.mode.is_a?(CompletionListMode)
|
|
@@ -97,15 +96,13 @@ module Textbringer
|
|
|
97
96
|
xs.each do |x|
|
|
98
97
|
completions.insert(x + "\n")
|
|
99
98
|
end
|
|
99
|
+
completions.beginning_of_buffer
|
|
100
100
|
COMPLETION[:completions_window].buffer = completions
|
|
101
101
|
ensure
|
|
102
102
|
completions.read_only = true
|
|
103
103
|
end
|
|
104
104
|
else
|
|
105
|
-
|
|
106
|
-
COMPLETION[:completions_window].buffer =
|
|
107
|
-
COMPLETION[:original_buffer]
|
|
108
|
-
end
|
|
105
|
+
delete_completions_window
|
|
109
106
|
end
|
|
110
107
|
end
|
|
111
108
|
private :update_completions
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
module Textbringer::Buffer::RectangleMethods
|
|
2
|
+
SHARED_VALUES = {}
|
|
3
|
+
|
|
4
|
+
refine Textbringer::Buffer do
|
|
5
|
+
# Returns start_line, start_col, end_line, and end_col of the rectangle region
|
|
6
|
+
# Note that start_col and end_col are 0-origin and width-based (neither 1-origin nor codepoint-based)
|
|
7
|
+
def rectangle_boundaries(s = @point, e = mark)
|
|
8
|
+
s, e = Buffer.region_boundaries(s, e)
|
|
9
|
+
save_excursion do
|
|
10
|
+
goto_char(s)
|
|
11
|
+
start_line = @current_line
|
|
12
|
+
beginning_of_line
|
|
13
|
+
start_col = display_width(substring(@point, s))
|
|
14
|
+
goto_char(e)
|
|
15
|
+
end_line = @current_line
|
|
16
|
+
beginning_of_line
|
|
17
|
+
end_col = display_width(substring(@point, e))
|
|
18
|
+
|
|
19
|
+
# Ensure start_col <= end_col
|
|
20
|
+
if start_col > end_col
|
|
21
|
+
start_col, end_col = end_col, start_col
|
|
22
|
+
end
|
|
23
|
+
[start_line, start_col, end_line, end_col]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply_on_rectangle(s = @point, e = mark)
|
|
28
|
+
start_line, start_col, end_line, end_col = rectangle_boundaries(s, e)
|
|
29
|
+
|
|
30
|
+
save_excursion do
|
|
31
|
+
composite_edit do
|
|
32
|
+
goto_line(start_line)
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
beginning_of_line
|
|
36
|
+
line_start = @point
|
|
37
|
+
|
|
38
|
+
# Move to start column
|
|
39
|
+
col = 0
|
|
40
|
+
while col < start_col && !end_of_line?
|
|
41
|
+
forward_char
|
|
42
|
+
col = display_width(substring(line_start, @point))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
yield(start_col, end_col, col, line_start, start_line, end_line)
|
|
46
|
+
|
|
47
|
+
# Move to next line for forward iteration
|
|
48
|
+
break if @current_line >= end_line
|
|
49
|
+
forward_line
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_rectangle(s = @point, e = mark)
|
|
56
|
+
lines = []
|
|
57
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
58
|
+
start_pos = @point
|
|
59
|
+
width = end_col - start_col
|
|
60
|
+
|
|
61
|
+
# If we haven't reached start_col, the line is too short
|
|
62
|
+
if col < start_col
|
|
63
|
+
# Line is shorter than start column, extract all spaces
|
|
64
|
+
lines << " " * width
|
|
65
|
+
else
|
|
66
|
+
# Move to end column
|
|
67
|
+
while col < end_col && !end_of_line?
|
|
68
|
+
forward_char
|
|
69
|
+
col = display_width(substring(line_start, @point))
|
|
70
|
+
end
|
|
71
|
+
end_pos = @point
|
|
72
|
+
|
|
73
|
+
# Extract the rectangle text for this line
|
|
74
|
+
if end_pos > start_pos
|
|
75
|
+
extracted = substring(start_pos, end_pos)
|
|
76
|
+
# Pad with spaces if the extracted text is shorter than rectangle width
|
|
77
|
+
extracted_width = display_width(extracted)
|
|
78
|
+
if extracted_width < width
|
|
79
|
+
extracted += " " * (width - extracted_width)
|
|
80
|
+
end
|
|
81
|
+
lines << extracted
|
|
82
|
+
else
|
|
83
|
+
lines << " " * width
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
lines
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def copy_rectangle(s = @point, e = mark)
|
|
92
|
+
lines = extract_rectangle(s, e)
|
|
93
|
+
SHARED_VALUES[:killed_rectangle] = lines
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def kill_rectangle(s = @point, e = mark)
|
|
97
|
+
copy_rectangle(s, e)
|
|
98
|
+
delete_rectangle(s, e)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def delete_rectangle(s = @point, e = mark)
|
|
102
|
+
check_read_only_flag
|
|
103
|
+
|
|
104
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
105
|
+
start_pos = @point
|
|
106
|
+
|
|
107
|
+
# Only delete if we're within the line bounds
|
|
108
|
+
if col >= start_col
|
|
109
|
+
# Move to end column
|
|
110
|
+
while col < end_col && !end_of_line?
|
|
111
|
+
forward_char
|
|
112
|
+
col = display_width(substring(line_start, @point))
|
|
113
|
+
end
|
|
114
|
+
end_pos = @point
|
|
115
|
+
|
|
116
|
+
# Delete the rectangle text for this line
|
|
117
|
+
if end_pos > start_pos
|
|
118
|
+
delete_region(start_pos, end_pos)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def yank_rectangle
|
|
125
|
+
raise "No rectangle in kill ring" if SHARED_VALUES[:killed_rectangle].nil?
|
|
126
|
+
lines = SHARED_VALUES[:killed_rectangle]
|
|
127
|
+
start_line = @current_line
|
|
128
|
+
start_point = @point
|
|
129
|
+
start_col = save_excursion {
|
|
130
|
+
beginning_of_line
|
|
131
|
+
display_width(substring(@point, start_point))
|
|
132
|
+
}
|
|
133
|
+
composite_edit do
|
|
134
|
+
lines.each_with_index do |line, i|
|
|
135
|
+
goto_line(start_line + i)
|
|
136
|
+
beginning_of_line
|
|
137
|
+
line_start = @point
|
|
138
|
+
|
|
139
|
+
# Move to start column, extending line if necessary
|
|
140
|
+
col = 0
|
|
141
|
+
while col < start_col && !end_of_line?
|
|
142
|
+
forward_char
|
|
143
|
+
col = display_width(substring(line_start, @point))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# If line is shorter than start_col, extend it with spaces
|
|
147
|
+
if col < start_col
|
|
148
|
+
insert(" " * (start_col - col))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Insert the rectangle line
|
|
152
|
+
insert(line)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def open_rectangle(s = @point, e = mark)
|
|
158
|
+
check_read_only_flag
|
|
159
|
+
s, e = Buffer.region_boundaries(s, e)
|
|
160
|
+
composite_edit do
|
|
161
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
162
|
+
# If line is shorter than start_col, extend it with spaces
|
|
163
|
+
if col < start_col
|
|
164
|
+
insert(" " * (start_col - col))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Insert spaces to create the rectangle
|
|
168
|
+
insert(" " * (end_col - start_col))
|
|
169
|
+
end
|
|
170
|
+
goto_char(s)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def clear_rectangle(s = @point, e = mark)
|
|
175
|
+
check_read_only_flag
|
|
176
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
177
|
+
start_pos = @point
|
|
178
|
+
if col < start_col
|
|
179
|
+
insert(" " * (end_col - start_col))
|
|
180
|
+
else
|
|
181
|
+
while col < end_col && !end_of_line?
|
|
182
|
+
forward_char
|
|
183
|
+
col = display_width(substring(line_start, @point))
|
|
184
|
+
end
|
|
185
|
+
end_pos = @point
|
|
186
|
+
|
|
187
|
+
delete_region(start_pos, end_pos) if end_pos > start_pos
|
|
188
|
+
insert(" " * (end_col - start_col))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def string_rectangle(str, s = @point, e = mark)
|
|
194
|
+
check_read_only_flag
|
|
195
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
196
|
+
start_pos = @point
|
|
197
|
+
if col < start_col
|
|
198
|
+
insert(" " * (start_col - col))
|
|
199
|
+
insert(str)
|
|
200
|
+
else
|
|
201
|
+
while col < end_col && !end_of_line?
|
|
202
|
+
forward_char
|
|
203
|
+
col = display_width(substring(line_start, @point))
|
|
204
|
+
end
|
|
205
|
+
end_pos = @point
|
|
206
|
+
|
|
207
|
+
delete_region(start_pos, end_pos) if end_pos > start_pos
|
|
208
|
+
insert(str)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def string_insert_rectangle(str, s = @point, e = mark)
|
|
214
|
+
check_read_only_flag
|
|
215
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start|
|
|
216
|
+
if col < start_col
|
|
217
|
+
insert(" " * (start_col - col))
|
|
218
|
+
end
|
|
219
|
+
insert(str)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def rectangle_number_lines(s = @point, e = mark)
|
|
224
|
+
check_read_only_flag
|
|
225
|
+
n = 1
|
|
226
|
+
number_width = nil
|
|
227
|
+
apply_on_rectangle(s, e) do |start_col, end_col, col, line_start, start_line, end_line|
|
|
228
|
+
number_width ||= (1 + (end_line - start_line)).to_s.size
|
|
229
|
+
if col < start_col
|
|
230
|
+
insert(" " * (start_col - col))
|
|
231
|
+
end
|
|
232
|
+
insert(n.to_s.rjust(number_width) + " ")
|
|
233
|
+
n += 1
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
using Textbringer::Buffer::RectangleMethods
|
|
240
|
+
|
|
241
|
+
module Textbringer
|
|
242
|
+
module Commands
|
|
243
|
+
define_command(:kill_rectangle,
|
|
244
|
+
doc: "Kill the text of the region-rectangle, saving its contents as the last killed rectangle.") do
|
|
245
|
+
Buffer.current.kill_rectangle
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
define_command(:copy_rectangle_as_kill,
|
|
249
|
+
doc: "Save the text of the region-rectangle as the last killed rectangle.") do
|
|
250
|
+
Buffer.current.copy_rectangle
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
define_command(:delete_rectangle,
|
|
254
|
+
doc: "Delete the text of the region-rectangle.") do
|
|
255
|
+
Buffer.current.delete_rectangle
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
define_command(:yank_rectangle,
|
|
259
|
+
doc: "Yank the last killed rectangle with its upper left corner at point.") do
|
|
260
|
+
Buffer.current.yank_rectangle
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
define_command(:open_rectangle,
|
|
264
|
+
doc: "Insert blank space to fill the space of the region-rectangle. This pushes the previous contents of the region-rectangle to the right.") do
|
|
265
|
+
Buffer.current.open_rectangle
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
define_command(:clear_rectangle,
|
|
269
|
+
doc: "Clear the region-rectangle by replacing its contents with spaces.") do
|
|
270
|
+
Buffer.current.clear_rectangle
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
define_command(:string_rectangle,
|
|
274
|
+
doc: "Replace rectangle contents with the specified string on each line.") do
|
|
275
|
+
|str = read_from_minibuffer("String rectangle: ")|
|
|
276
|
+
Buffer.current.string_rectangle(str)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
define_command(:string_insert_rectangle,
|
|
280
|
+
doc: "Insert the specified string on each line of the rectangle.") do
|
|
281
|
+
|str = read_from_minibuffer("String insert rectangle: ")|
|
|
282
|
+
Buffer.current.string_insert_rectangle(str)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
define_command(:rectangle_number_lines,
|
|
286
|
+
doc: "Insert numbers in front of the region-rectangle.") do
|
|
287
|
+
Buffer.current.rectangle_number_lines
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -42,7 +42,10 @@ module Textbringer
|
|
|
42
42
|
loop do
|
|
43
43
|
re_search_forward(regexp)
|
|
44
44
|
Window.current.recenter_if_needed
|
|
45
|
-
|
|
45
|
+
# Don't update visible_mark if mark is already active (transient mark mode)
|
|
46
|
+
unless Buffer.current.mark_active?
|
|
47
|
+
Buffer.current.set_visible_mark(match_beginning(0))
|
|
48
|
+
end
|
|
46
49
|
begin
|
|
47
50
|
Window.redisplay
|
|
48
51
|
c = read_single_char("Replace?", [?y, ?n, ?!, ?q, ?.])
|
|
@@ -66,7 +69,10 @@ module Textbringer
|
|
|
66
69
|
break
|
|
67
70
|
end
|
|
68
71
|
ensure
|
|
69
|
-
|
|
72
|
+
# Don't delete visible_mark if mark is active (transient mark mode)
|
|
73
|
+
unless Buffer.current.mark_active?
|
|
74
|
+
Buffer.current.delete_visible_mark
|
|
75
|
+
end
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
rescue SearchError
|
data/lib/textbringer/config.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Textbringer
|
|
|
15
15
|
shell_file_name: ENV["SHELL"],
|
|
16
16
|
shell_command_switch: "-c",
|
|
17
17
|
grep_command: "grep -nH -e",
|
|
18
|
+
ispell_command: "aspell -a",
|
|
18
19
|
fill_column: 70,
|
|
19
20
|
read_file_name_completion_ignore_case: RUBY_PLATFORM.match?(/darwin/),
|
|
20
21
|
default_input_method: "t_code"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
# Base class for global minor modes that affect all buffers.
|
|
3
|
+
# Unlike buffer-local MinorMode, global minor modes have a single on/off state.
|
|
4
|
+
class GlobalMinorMode
|
|
5
|
+
extend Commands
|
|
6
|
+
include Commands
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :mode_name
|
|
10
|
+
attr_accessor :command_name
|
|
11
|
+
|
|
12
|
+
def enabled=(val)
|
|
13
|
+
@enabled = val
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enabled? = @enabled
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.inherited(child)
|
|
20
|
+
# Initialize enabled to false immediately
|
|
21
|
+
child.instance_variable_set(:@enabled, false)
|
|
22
|
+
|
|
23
|
+
class_name = child.name
|
|
24
|
+
if class_name.nil? || class_name.empty?
|
|
25
|
+
raise ArgumentError, "GlobalMinorMode subclasses must be named classes (anonymous classes are not supported)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
base_name = class_name.slice(/[^:]*\z/)
|
|
29
|
+
child.mode_name = base_name.sub(/Mode\z/, "")
|
|
30
|
+
command_name = base_name.sub(/\A[A-Z]/) { |s| s.downcase }.
|
|
31
|
+
gsub(/(?<=[a-z])([A-Z])/) {
|
|
32
|
+
"_" + $1.downcase
|
|
33
|
+
}
|
|
34
|
+
command = command_name.intern
|
|
35
|
+
child.command_name = command
|
|
36
|
+
|
|
37
|
+
# Define the toggle command
|
|
38
|
+
define_command(command) do
|
|
39
|
+
if child.enabled?
|
|
40
|
+
child.disable
|
|
41
|
+
child.enabled = false
|
|
42
|
+
else
|
|
43
|
+
child.enable
|
|
44
|
+
child.enabled = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Override these in subclasses
|
|
50
|
+
def self.enable
|
|
51
|
+
raise EditorError, "Subclass must implement enable"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.disable
|
|
55
|
+
raise EditorError, "Subclass must implement disable"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/textbringer/keymap.rb
CHANGED
|
@@ -209,6 +209,8 @@ module Textbringer
|
|
|
209
209
|
GLOBAL_MAP.define_key("\C-xry", :yank_rectangle)
|
|
210
210
|
GLOBAL_MAP.define_key("\C-xro", :open_rectangle)
|
|
211
211
|
GLOBAL_MAP.define_key("\C-xrc", :clear_rectangle)
|
|
212
|
+
GLOBAL_MAP.define_key("\C-xrt", :string_rectangle)
|
|
213
|
+
GLOBAL_MAP.define_key("\C-xrN", :rectangle_number_lines)
|
|
212
214
|
GLOBAL_MAP.define_key("\C-x(", :start_keyboard_macro)
|
|
213
215
|
GLOBAL_MAP.define_key(:f3, :start_keyboard_macro)
|
|
214
216
|
GLOBAL_MAP.define_key("\C-x)", :end_keyboard_macro)
|
|
@@ -24,10 +24,7 @@ module Textbringer
|
|
|
24
24
|
if s.size > 0
|
|
25
25
|
Window.current = Window.echo_area
|
|
26
26
|
complete_minibuffer_with_string(s)
|
|
27
|
-
|
|
28
|
-
COMPLETION[:completions_window].buffer =
|
|
29
|
-
COMPLETION[:original_buffer]
|
|
30
|
-
end
|
|
27
|
+
delete_completions_window
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
end
|