kward 0.71.0 → 0.72.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 +4 -4
- data/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Built-in composer file editor behavior.
|
|
6
|
+
module EditorController
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def editor_active?
|
|
10
|
+
!@editor_state.nil?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def open_selected_file_in_editor(fallback_to_typed_path: false)
|
|
14
|
+
path = selected_file_open_path
|
|
15
|
+
if path
|
|
16
|
+
opened = open_editor(path)
|
|
17
|
+
add_history(history_file_open_command(path)) if opened
|
|
18
|
+
return opened
|
|
19
|
+
end
|
|
20
|
+
return false unless fallback_to_typed_path
|
|
21
|
+
|
|
22
|
+
open_typed_file_path_in_editor
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def open_typed_file_path_in_editor
|
|
26
|
+
file_open = active_file_open
|
|
27
|
+
return false unless file_open
|
|
28
|
+
if file_open[:query].empty?
|
|
29
|
+
@file_editor_open_status = "Type a file path after $"
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opened = open_editor(file_open[:query], allow_new: true)
|
|
34
|
+
add_history(history_file_open_command(file_open[:query])) if opened
|
|
35
|
+
opened
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def history_file_open_command(path)
|
|
39
|
+
full_path = File.expand_path(path.to_s, Dir.pwd)
|
|
40
|
+
relative_path = Pathname.new(full_path).relative_path_from(Pathname.new(File.expand_path(Dir.pwd))).to_s
|
|
41
|
+
"$#{relative_path}"
|
|
42
|
+
rescue ArgumentError
|
|
43
|
+
"$#{path}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def open_editor(path, allow_new: false, base_dir: Dir.pwd, restrict_to_workspace: true)
|
|
47
|
+
full_path = File.expand_path(path.to_s, base_dir)
|
|
48
|
+
root = File.expand_path(Dir.pwd)
|
|
49
|
+
if restrict_to_workspace && !(full_path == root || full_path.start_with?("#{root}/"))
|
|
50
|
+
@file_editor_open_status = "Cannot edit file outside workspace"
|
|
51
|
+
return false
|
|
52
|
+
end
|
|
53
|
+
if File.exist?(full_path) && !File.file?(full_path)
|
|
54
|
+
@file_editor_open_status = "Cannot edit non-file path: #{path}"
|
|
55
|
+
return false
|
|
56
|
+
end
|
|
57
|
+
unless File.exist?(full_path)
|
|
58
|
+
unless allow_new
|
|
59
|
+
@file_editor_open_status = "Cannot edit missing file: #{path}"
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
parent = File.dirname(full_path)
|
|
63
|
+
unless Dir.exist?(parent)
|
|
64
|
+
@file_editor_open_status = "Cannot create file; parent directory is missing"
|
|
65
|
+
return false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@editor_state = EditorState.new(path: full_path, content: File.exist?(full_path) ? File.read(full_path) : "", new_file: !File.exist?(full_path), editor_mode: current_editor_mode)
|
|
70
|
+
@prompt_label = "Edit>"
|
|
71
|
+
self.composer_input = ""
|
|
72
|
+
self.composer_cursor = 0
|
|
73
|
+
@composer.clear_attachments
|
|
74
|
+
@pending_keys.clear
|
|
75
|
+
@file_overlay_dismissed_token = nil
|
|
76
|
+
@file_open_dismissed_token = nil
|
|
77
|
+
@asking = true
|
|
78
|
+
set_editor_bar_cursor_locked if current_editor_bar_cursor?
|
|
79
|
+
enable_editor_mouse_reporting
|
|
80
|
+
true
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
@file_editor_open_status = "Cannot edit #{path}: #{e.message}"
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def open_diff_viewer(path, content)
|
|
87
|
+
@editor_state = EditorState.new(path: path.to_s, content: content.to_s, new_file: true, editor_mode: current_editor_mode, readonly: true, diff_view: true)
|
|
88
|
+
@prompt_label = "Diff>"
|
|
89
|
+
self.composer_input = ""
|
|
90
|
+
self.composer_cursor = 0
|
|
91
|
+
@composer.clear_attachments
|
|
92
|
+
@pending_keys.clear
|
|
93
|
+
@asking = true
|
|
94
|
+
enable_editor_mouse_reporting
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def current_editor_mode
|
|
99
|
+
return normalize_editor_mode(@editor_mode_source.call) if @editor_mode_source.respond_to?(:call)
|
|
100
|
+
|
|
101
|
+
@editor_mode
|
|
102
|
+
rescue StandardError
|
|
103
|
+
@editor_mode
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def current_editor_soft_wrap?
|
|
107
|
+
return @editor_soft_wrap_source.call != false if @editor_soft_wrap_source.respond_to?(:call)
|
|
108
|
+
|
|
109
|
+
@editor_soft_wrap != false
|
|
110
|
+
rescue StandardError
|
|
111
|
+
@editor_soft_wrap != false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def current_editor_bar_cursor?
|
|
115
|
+
return @editor_bar_cursor_source.call != false if @editor_bar_cursor_source.respond_to?(:call)
|
|
116
|
+
|
|
117
|
+
@editor_bar_cursor != false
|
|
118
|
+
rescue StandardError
|
|
119
|
+
@editor_bar_cursor != false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def current_editor_line_numbers
|
|
123
|
+
return normalize_editor_line_numbers(@editor_line_numbers_source.call) if @editor_line_numbers_source.respond_to?(:call)
|
|
124
|
+
|
|
125
|
+
normalize_editor_line_numbers(@editor_line_numbers)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
normalize_editor_line_numbers(@editor_line_numbers)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def close_editor
|
|
131
|
+
disable_editor_mouse_reporting(force: true)
|
|
132
|
+
restore_editor_cursor_shape_locked
|
|
133
|
+
@editor_text_width = nil
|
|
134
|
+
@editor_state = nil
|
|
135
|
+
@prompt_label = "You>"
|
|
136
|
+
self.composer_input = ""
|
|
137
|
+
self.composer_cursor = 0
|
|
138
|
+
restore_project_browser_after_editor_close
|
|
139
|
+
@asking = true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def handle_editor_key(key)
|
|
143
|
+
return if key.nil?
|
|
144
|
+
mouse_result = handle_editor_mouse_key(key)
|
|
145
|
+
return mouse_result unless mouse_result == false
|
|
146
|
+
return handle_readonly_editor_key(key) if @editor_state&.readonly?
|
|
147
|
+
return handle_vibe_key(key) if @editor_state&.vibe?
|
|
148
|
+
return handle_emacs_key(key) if @editor_state&.emacs?
|
|
149
|
+
return handle_modern_key(key) if @editor_state&.modern?
|
|
150
|
+
return if handle_editor_bracketed_paste_key(key)
|
|
151
|
+
|
|
152
|
+
csi_result = handle_editor_csi_u_key(key)
|
|
153
|
+
return csi_result unless csi_result == false
|
|
154
|
+
|
|
155
|
+
shift_result = handle_editor_shift_navigation_key(key)
|
|
156
|
+
return shift_result unless shift_result == false
|
|
157
|
+
|
|
158
|
+
binding_result = handle_editor_key_binding(key)
|
|
159
|
+
return binding_result unless binding_result == false
|
|
160
|
+
|
|
161
|
+
editor_tab_result = handle_editor_tab_key(key)
|
|
162
|
+
return editor_tab_result unless editor_tab_result == false
|
|
163
|
+
|
|
164
|
+
tab_result = handle_tab_key_binding(key)
|
|
165
|
+
return tab_result unless tab_result == false
|
|
166
|
+
|
|
167
|
+
return true if handle_bundled_key(key) { |token| handle_editor_key(token) }
|
|
168
|
+
|
|
169
|
+
case key
|
|
170
|
+
when "\n", "\r"
|
|
171
|
+
return editor_search_confirm if editor_search_active?
|
|
172
|
+
clear_editor_selection_before_edit
|
|
173
|
+
editor_insert_newline
|
|
174
|
+
when "\t"
|
|
175
|
+
editor_insert_tab unless editor_search_active?
|
|
176
|
+
when "\b", "\x7F"
|
|
177
|
+
editor_search_active? ? editor_search_delete_character : delete_editor_selection || editor_delete_before_cursor
|
|
178
|
+
when "\x03"
|
|
179
|
+
return editor_search_cancel if editor_search_active?
|
|
180
|
+
when "\e"
|
|
181
|
+
return editor_search_cancel if editor_search_active?
|
|
182
|
+
return @editor_state.clear_selection if @editor_state.selection_active?
|
|
183
|
+
when "\x11"
|
|
184
|
+
quit_editor
|
|
185
|
+
when "\x13"
|
|
186
|
+
save_editor
|
|
187
|
+
when "/"
|
|
188
|
+
clear_editor_selection_before_edit unless editor_search_active?
|
|
189
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
190
|
+
else
|
|
191
|
+
key_name = key_name_for(key)
|
|
192
|
+
named_result = handle_editor_named_key(key_name) if key_name
|
|
193
|
+
return named_result unless named_result == false || named_result.nil?
|
|
194
|
+
|
|
195
|
+
if editor_search_active?
|
|
196
|
+
editor_search_append(key) if printable_key?(key)
|
|
197
|
+
elsif printable_key?(key)
|
|
198
|
+
editor_insert_printable(key)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def handle_editor_csi_u_key(key)
|
|
204
|
+
sequence = parse_csi_u_key(key)
|
|
205
|
+
return false unless sequence
|
|
206
|
+
|
|
207
|
+
handle_parsed_editor_csi_u_key(sequence)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def handle_parsed_editor_csi_u_key(sequence)
|
|
211
|
+
code = sequence[:code]
|
|
212
|
+
modifier = sequence[:modifier]
|
|
213
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
214
|
+
|
|
215
|
+
binding_result = handle_editor_modified_csi_u_key(code, modifier)
|
|
216
|
+
return binding_result unless binding_result == false
|
|
217
|
+
text = csi_u_printable_text(sequence)
|
|
218
|
+
return editor_insert_csi_u_text(text) if text
|
|
219
|
+
return true if csi_u_text_field?(sequence)
|
|
220
|
+
|
|
221
|
+
case code
|
|
222
|
+
when 9
|
|
223
|
+
return false if editor_search_active?
|
|
224
|
+
return false if ctrl_modifier?(modifier) || alt_modifier?(modifier) || super_modifier?(modifier)
|
|
225
|
+
|
|
226
|
+
shift_modifier?(modifier) ? editor_outdent_tab : editor_insert_tab
|
|
227
|
+
when 13
|
|
228
|
+
clear_editor_selection_before_edit unless editor_search_active?
|
|
229
|
+
editor_search_active? ? editor_search_confirm : editor_insert_newline
|
|
230
|
+
when 27
|
|
231
|
+
editor_search_active? ? editor_search_cancel : @editor_state.clear_selection
|
|
232
|
+
when 8, 127
|
|
233
|
+
editor_search_active? ? editor_search_delete_character : delete_editor_selection || editor_delete_before_cursor
|
|
234
|
+
nil
|
|
235
|
+
when 4
|
|
236
|
+
delete_editor_selection || @editor_state.delete_at_cursor unless editor_search_active?
|
|
237
|
+
nil
|
|
238
|
+
else
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def editor_insert_csi_u_text(text)
|
|
244
|
+
if editor_search_active?
|
|
245
|
+
editor_search_append(text)
|
|
246
|
+
else
|
|
247
|
+
editor_insert_printable(text)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def handle_editor_mouse_key(key)
|
|
252
|
+
event = parse_editor_mouse_key(key)
|
|
253
|
+
return false unless event
|
|
254
|
+
|
|
255
|
+
queue_pending_keys(event[:remaining]) unless event[:remaining].empty?
|
|
256
|
+
case event[:code]
|
|
257
|
+
when 64
|
|
258
|
+
scroll_editor_up(editor_mouse_scroll_rows)
|
|
259
|
+
when 65
|
|
260
|
+
scroll_editor_down(editor_mouse_scroll_rows)
|
|
261
|
+
else
|
|
262
|
+
if event[:drag]
|
|
263
|
+
handle_editor_mouse_drag(event)
|
|
264
|
+
elsif event[:button].zero?
|
|
265
|
+
event[:release] ? finish_editor_mouse_drag : handle_editor_mouse_press(event)
|
|
266
|
+
else
|
|
267
|
+
true
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def parse_editor_mouse_key(key)
|
|
273
|
+
match = key.to_s.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])/)
|
|
274
|
+
return nil unless match
|
|
275
|
+
|
|
276
|
+
code = match[1].to_i
|
|
277
|
+
{
|
|
278
|
+
code: code,
|
|
279
|
+
button: code & 3,
|
|
280
|
+
column: match[2].to_i,
|
|
281
|
+
row: match[3].to_i,
|
|
282
|
+
action: match[4],
|
|
283
|
+
release: match[4] == "m",
|
|
284
|
+
drag: (code & 32).positive?,
|
|
285
|
+
remaining: key.to_s[match[0].length..].to_s
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def handle_editor_mouse_press(event)
|
|
290
|
+
position = editor_position_for_mouse_event(event)
|
|
291
|
+
return true unless position
|
|
292
|
+
|
|
293
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
294
|
+
click_count = editor_mouse_click_count(event, now)
|
|
295
|
+
case click_count
|
|
296
|
+
when 3..Float::INFINITY
|
|
297
|
+
range = select_editor_line_at(position[:line])
|
|
298
|
+
@editor_mouse_drag_anchor = range[0]
|
|
299
|
+
@editor_mouse_dragging = true
|
|
300
|
+
when 2
|
|
301
|
+
range = select_editor_word_at(position[:offset])
|
|
302
|
+
if range
|
|
303
|
+
@editor_mouse_drag_anchor = range[0]
|
|
304
|
+
@editor_mouse_dragging = true
|
|
305
|
+
else
|
|
306
|
+
finish_editor_mouse_drag
|
|
307
|
+
end
|
|
308
|
+
else
|
|
309
|
+
@editor_state.clear_selection
|
|
310
|
+
@editor_state.cursor = position[:offset]
|
|
311
|
+
@editor_mouse_drag_anchor = position[:offset]
|
|
312
|
+
@editor_mouse_dragging = true
|
|
313
|
+
end
|
|
314
|
+
@editor_last_click = { time: now, column: event[:column], row: event[:row], count: click_count }
|
|
315
|
+
true
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def handle_editor_mouse_drag(event)
|
|
319
|
+
return true unless @editor_mouse_dragging
|
|
320
|
+
|
|
321
|
+
position = editor_drag_position_for_mouse_event(event)
|
|
322
|
+
return true unless position
|
|
323
|
+
|
|
324
|
+
@editor_state.selection_anchor = @editor_mouse_drag_anchor
|
|
325
|
+
@editor_state.cursor = position[:offset]
|
|
326
|
+
true
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def finish_editor_mouse_drag
|
|
330
|
+
@editor_mouse_dragging = false
|
|
331
|
+
@editor_mouse_drag_anchor = nil
|
|
332
|
+
true
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def editor_mouse_click_count(event, now)
|
|
336
|
+
return 1 unless editor_repeated_click?(event, now)
|
|
337
|
+
|
|
338
|
+
@editor_last_click[:count].to_i + 1
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def editor_repeated_click?(event, now)
|
|
342
|
+
return false unless @editor_last_click
|
|
343
|
+
return false unless now - @editor_last_click[:time] <= 0.5
|
|
344
|
+
|
|
345
|
+
(@editor_last_click[:column] - event[:column]).abs <= 1 && (@editor_last_click[:row] - event[:row]).abs <= 1
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def select_editor_word_at(offset)
|
|
349
|
+
range = @editor_state.word_range_at(offset)
|
|
350
|
+
return @editor_state.clear_selection unless range
|
|
351
|
+
|
|
352
|
+
@editor_state.selection_anchor = range[0]
|
|
353
|
+
@editor_state.cursor = range[1]
|
|
354
|
+
range
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def select_editor_line_at(line_index)
|
|
358
|
+
range = @editor_state.line_range(line_index)
|
|
359
|
+
@editor_state.selection_anchor = range[0]
|
|
360
|
+
@editor_state.cursor = range[1]
|
|
361
|
+
range
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def editor_drag_position_for_mouse_event(event)
|
|
365
|
+
scroll_editor_horizontally_for_drag(event)
|
|
366
|
+
top = editor_mouse_content_top_row
|
|
367
|
+
bottom = top + editor_visible_line_count - 1
|
|
368
|
+
if event[:row] < top
|
|
369
|
+
scroll_editor_up(editor_mouse_scroll_rows)
|
|
370
|
+
return editor_edge_position_for_mouse_event(event, @editor_state.viewport_row)
|
|
371
|
+
elsif event[:row] > bottom
|
|
372
|
+
scroll_editor_down(editor_mouse_scroll_rows)
|
|
373
|
+
return editor_edge_position_for_mouse_event(event, @editor_state.viewport_row + editor_visible_line_count - 1)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
editor_position_for_mouse_event(event)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def editor_position_for_mouse_event(event)
|
|
380
|
+
row_offset = event[:row] - editor_mouse_content_top_row
|
|
381
|
+
return nil if row_offset.negative? || row_offset >= editor_visible_line_count
|
|
382
|
+
|
|
383
|
+
if current_editor_soft_wrap?
|
|
384
|
+
editor_wrapped_position_for_mouse(event, row_offset)
|
|
385
|
+
else
|
|
386
|
+
line_index = @editor_state.viewport_row + row_offset
|
|
387
|
+
editor_position_for_line_and_column(line_index, editor_mouse_column_for_event(event))
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def editor_wrapped_position_for_mouse(event, row_offset)
|
|
392
|
+
editor_wrapped_position_for_visual_row(event, @editor_state.viewport_row + row_offset)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def editor_edge_position_for_mouse_event(event, row_index)
|
|
396
|
+
if current_editor_soft_wrap?
|
|
397
|
+
editor_wrapped_position_for_visual_row(event, row_index)
|
|
398
|
+
else
|
|
399
|
+
editor_position_for_line_and_column(row_index, editor_mouse_column_for_event(event))
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def editor_wrapped_position_for_visual_row(event, row_index)
|
|
404
|
+
visual_row = editor_visual_rows(current_editor_text_width)[row_index]
|
|
405
|
+
return nil unless visual_row
|
|
406
|
+
|
|
407
|
+
column = visual_row[:column_offset] + editor_mouse_column_for_event(event)
|
|
408
|
+
editor_position_for_line_and_column(visual_row[:line_index], column)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def editor_position_for_line_and_column(line_index, column)
|
|
412
|
+
lines = @editor_state.lines
|
|
413
|
+
line_index = [[line_index.to_i, 0].max, lines.length - 1].min
|
|
414
|
+
column = [[column.to_i, 0].max, lines[line_index].to_s.length].min
|
|
415
|
+
@editor_state.set_cursor_line_and_column(line_index, column)
|
|
416
|
+
{ line: line_index, column: column, offset: @editor_state.cursor }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def editor_bottom_mouse_line_index
|
|
420
|
+
[@editor_state.viewport_row + editor_visible_line_count - 1, @editor_state.lines.length - 1].min
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def scroll_editor_horizontally_for_drag(event)
|
|
424
|
+
return if current_editor_soft_wrap?
|
|
425
|
+
|
|
426
|
+
if event[:column] < editor_mouse_text_left_column
|
|
427
|
+
@editor_state.viewport_column = [@editor_state.viewport_column.to_i - editor_mouse_scroll_rows, 0].max
|
|
428
|
+
elsif event[:column] > editor_mouse_text_right_column
|
|
429
|
+
@editor_state.viewport_column = @editor_state.viewport_column.to_i + editor_mouse_scroll_rows
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def editor_mouse_column_for_event(event)
|
|
434
|
+
column = [event[:column] - editor_mouse_text_left_column, 0].max
|
|
435
|
+
current_editor_soft_wrap? ? column : column + @editor_state.viewport_column.to_i
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def editor_mouse_text_left_column
|
|
439
|
+
3 + editor_line_number_gutter_width
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def editor_mouse_text_right_column
|
|
443
|
+
editor_mouse_text_left_column + current_editor_text_width - 1
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def editor_mouse_content_top_row
|
|
447
|
+
3
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def handle_editor_shift_navigation_key(key)
|
|
451
|
+
return false if editor_search_active?
|
|
452
|
+
|
|
453
|
+
case key
|
|
454
|
+
when "\e[1;2D", "\e[2D"
|
|
455
|
+
editor_extending_selection { @editor_state.move_left }
|
|
456
|
+
when "\e[1;2C", "\e[2C"
|
|
457
|
+
editor_extending_selection { @editor_state.move_right }
|
|
458
|
+
when "\e[1;2A", "\e[2A"
|
|
459
|
+
editor_extending_selection { editor_move_up }
|
|
460
|
+
when "\e[1;2B", "\e[2B"
|
|
461
|
+
editor_extending_selection { editor_move_down }
|
|
462
|
+
else
|
|
463
|
+
false
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def handle_editor_key_binding(key)
|
|
468
|
+
case key
|
|
469
|
+
when "\x01"
|
|
470
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
471
|
+
when "\x02"
|
|
472
|
+
@editor_state.move_left unless editor_search_active?
|
|
473
|
+
when "\x04"
|
|
474
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
475
|
+
when "\x05"
|
|
476
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
477
|
+
when "\x06"
|
|
478
|
+
@editor_state.move_right unless editor_search_active?
|
|
479
|
+
when "\x00"
|
|
480
|
+
@editor_state.begin_selection unless editor_search_active?
|
|
481
|
+
when "\x0B"
|
|
482
|
+
@editor_state.kill_line_after_cursor unless editor_search_active?
|
|
483
|
+
when "\x0E"
|
|
484
|
+
editor_move_down unless editor_search_active?
|
|
485
|
+
when "\x10"
|
|
486
|
+
editor_move_up unless editor_search_active?
|
|
487
|
+
when "\x15"
|
|
488
|
+
@editor_state.kill_line_before_cursor unless editor_search_active?
|
|
489
|
+
when "\x17"
|
|
490
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
491
|
+
when "\x19"
|
|
492
|
+
editor_selection_active? ? copy_editor_selection : @editor_state.yank_kill_buffer unless editor_search_active?
|
|
493
|
+
when "\e[D", "\eOD"
|
|
494
|
+
@editor_state.move_left unless editor_search_active?
|
|
495
|
+
when "\e[C", "\eOC"
|
|
496
|
+
@editor_state.move_right unless editor_search_active?
|
|
497
|
+
when "\e[H", "\eOH", "\e[1~", "\e[7~"
|
|
498
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
499
|
+
when "\e[F", "\eOF", "\e[4~", "\e[8~"
|
|
500
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
501
|
+
when "\e[3~"
|
|
502
|
+
delete_editor_selection || @editor_state.delete_at_cursor unless editor_search_active?
|
|
503
|
+
when "\eb", "\eB"
|
|
504
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
505
|
+
when "\ef", "\eF"
|
|
506
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
507
|
+
when "\ed", "\eD"
|
|
508
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
509
|
+
when "\e\b", "\e\x7F"
|
|
510
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
511
|
+
else
|
|
512
|
+
handle_editor_modified_ansi_key(key) || false
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def handle_editor_modified_csi_u_key(code, modifier)
|
|
517
|
+
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
518
|
+
|
|
519
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
520
|
+
if ctrl_modifier?(modifier)
|
|
521
|
+
case normalized_code
|
|
522
|
+
when 32
|
|
523
|
+
@editor_state.begin_selection unless editor_search_active?
|
|
524
|
+
when 97
|
|
525
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
526
|
+
when 98
|
|
527
|
+
@editor_state.move_left unless editor_search_active?
|
|
528
|
+
when 99
|
|
529
|
+
editor_search_cancel if editor_search_active?
|
|
530
|
+
when 100
|
|
531
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
532
|
+
when 101
|
|
533
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
534
|
+
when 102
|
|
535
|
+
@editor_state.move_right unless editor_search_active?
|
|
536
|
+
when 107
|
|
537
|
+
@editor_state.kill_line_after_cursor unless editor_search_active?
|
|
538
|
+
when 110
|
|
539
|
+
editor_move_down unless editor_search_active?
|
|
540
|
+
when 112
|
|
541
|
+
editor_move_up unless editor_search_active?
|
|
542
|
+
when 113
|
|
543
|
+
quit_editor
|
|
544
|
+
when 115
|
|
545
|
+
save_editor
|
|
546
|
+
when 117
|
|
547
|
+
@editor_state.kill_line_before_cursor unless editor_search_active?
|
|
548
|
+
when 119
|
|
549
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
550
|
+
when 121
|
|
551
|
+
editor_selection_active? ? copy_editor_selection : @editor_state.yank_kill_buffer unless editor_search_active?
|
|
552
|
+
else
|
|
553
|
+
false
|
|
554
|
+
end
|
|
555
|
+
elsif alt_modifier?(modifier)
|
|
556
|
+
case normalized_code
|
|
557
|
+
when 98
|
|
558
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
559
|
+
when 100
|
|
560
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
561
|
+
when 102
|
|
562
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
563
|
+
else
|
|
564
|
+
false
|
|
565
|
+
end
|
|
566
|
+
else
|
|
567
|
+
false
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def handle_editor_modified_ansi_key(key)
|
|
572
|
+
sequence = parse_modified_ansi_key(key)
|
|
573
|
+
return false unless sequence
|
|
574
|
+
|
|
575
|
+
case sequence[:type]
|
|
576
|
+
when :cursor
|
|
577
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
578
|
+
|
|
579
|
+
case sequence[:final]
|
|
580
|
+
when "C"
|
|
581
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
582
|
+
when "D"
|
|
583
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
584
|
+
when "F"
|
|
585
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
586
|
+
when "H"
|
|
587
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
588
|
+
else
|
|
589
|
+
false
|
|
590
|
+
end
|
|
591
|
+
when :delete
|
|
592
|
+
if alt_modifier?(sequence[:modifier])
|
|
593
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
594
|
+
else
|
|
595
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
596
|
+
end
|
|
597
|
+
else
|
|
598
|
+
false
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def handle_editor_bracketed_paste_key(key)
|
|
603
|
+
paste = read_bracketed_paste(key)
|
|
604
|
+
return false unless paste
|
|
605
|
+
|
|
606
|
+
@editor_state.insert(normalize_paste(paste[:content])) unless editor_search_active?
|
|
607
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
608
|
+
true
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def ctrl_code(code)
|
|
612
|
+
value = code.to_i
|
|
613
|
+
return value if value < 32
|
|
614
|
+
|
|
615
|
+
value.chr.downcase.ord
|
|
616
|
+
rescue StandardError
|
|
617
|
+
code
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def handle_editor_named_key(key_name)
|
|
621
|
+
return false unless key_name
|
|
622
|
+
|
|
623
|
+
if editor_search_active?
|
|
624
|
+
case key_name
|
|
625
|
+
when :return, :enter
|
|
626
|
+
editor_search_confirm
|
|
627
|
+
when :backspace
|
|
628
|
+
editor_search_delete_character
|
|
629
|
+
else
|
|
630
|
+
false
|
|
631
|
+
end
|
|
632
|
+
else
|
|
633
|
+
case key_name
|
|
634
|
+
when :return, :enter
|
|
635
|
+
editor_insert_newline
|
|
636
|
+
when :backspace
|
|
637
|
+
delete_editor_selection || editor_delete_before_cursor
|
|
638
|
+
when :delete
|
|
639
|
+
delete_editor_selection || @editor_state.delete_at_cursor
|
|
640
|
+
when :left
|
|
641
|
+
@editor_state.move_left
|
|
642
|
+
when :right
|
|
643
|
+
@editor_state.move_right
|
|
644
|
+
when :up
|
|
645
|
+
editor_move_up
|
|
646
|
+
when :down
|
|
647
|
+
editor_move_down
|
|
648
|
+
when :home
|
|
649
|
+
@editor_state.move_line_start
|
|
650
|
+
when :end
|
|
651
|
+
@editor_state.move_line_end
|
|
652
|
+
when :pageup
|
|
653
|
+
scroll_editor_up(editor_scroll_page_rows)
|
|
654
|
+
when :pagedown
|
|
655
|
+
scroll_editor_down(editor_scroll_page_rows)
|
|
656
|
+
else
|
|
657
|
+
false
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def handle_readonly_editor_key(key)
|
|
663
|
+
return if handle_readonly_bracketed_paste_key(key)
|
|
664
|
+
|
|
665
|
+
return true if handle_bundled_key(key) { |token| handle_readonly_editor_key(token) }
|
|
666
|
+
|
|
667
|
+
key_name = key_name_for(key)
|
|
668
|
+
named_result = handle_readonly_named_key(key_name) if key_name
|
|
669
|
+
return named_result unless named_result == false || named_result.nil?
|
|
670
|
+
|
|
671
|
+
case key
|
|
672
|
+
when "\x11"
|
|
673
|
+
close_editor
|
|
674
|
+
when "\x06"
|
|
675
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
676
|
+
when "\x03"
|
|
677
|
+
editor_search_cancel if editor_search_active?
|
|
678
|
+
when "/"
|
|
679
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
680
|
+
when "\b", "\x7F"
|
|
681
|
+
editor_search_delete_character if editor_search_active?
|
|
682
|
+
when "\n", "\r"
|
|
683
|
+
editor_search_confirm if editor_search_active?
|
|
684
|
+
when "\e"
|
|
685
|
+
editor_search_active? ? editor_search_cancel : close_editor
|
|
686
|
+
else
|
|
687
|
+
csi_result = handle_readonly_csi_u_key(key)
|
|
688
|
+
return csi_result unless csi_result == false
|
|
689
|
+
|
|
690
|
+
if editor_search_active?
|
|
691
|
+
editor_search_append(key) if printable_key?(key)
|
|
692
|
+
elsif printable_key?(key)
|
|
693
|
+
@editor_state.status = "Read-only diff"
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def handle_readonly_csi_u_key(key)
|
|
699
|
+
sequence = parse_csi_u_key(key)
|
|
700
|
+
return false unless sequence
|
|
701
|
+
|
|
702
|
+
code = sequence[:code]
|
|
703
|
+
modifier = sequence[:modifier]
|
|
704
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
705
|
+
|
|
706
|
+
if ctrl_modifier?(modifier) && ctrl_code(code) == 102
|
|
707
|
+
return editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
case code
|
|
711
|
+
when 13
|
|
712
|
+
editor_search_confirm if editor_search_active?
|
|
713
|
+
when 27
|
|
714
|
+
editor_search_active? ? editor_search_cancel : close_editor
|
|
715
|
+
when 8, 127
|
|
716
|
+
editor_search_delete_character if editor_search_active?
|
|
717
|
+
else
|
|
718
|
+
return false unless editor_search_active?
|
|
719
|
+
|
|
720
|
+
text = csi_u_printable_text(sequence)
|
|
721
|
+
return true if text.nil? && csi_u_text_field?(sequence)
|
|
722
|
+
return false unless text
|
|
723
|
+
|
|
724
|
+
editor_search_append(text)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def handle_readonly_bracketed_paste_key(key)
|
|
729
|
+
paste = read_bracketed_paste(key)
|
|
730
|
+
return false unless paste
|
|
731
|
+
|
|
732
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
733
|
+
@editor_state.status = "Read-only diff" unless editor_search_active?
|
|
734
|
+
true
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def handle_readonly_named_key(key_name)
|
|
738
|
+
return false unless key_name
|
|
739
|
+
|
|
740
|
+
if editor_search_active?
|
|
741
|
+
case key_name
|
|
742
|
+
when :return, :enter
|
|
743
|
+
editor_search_confirm
|
|
744
|
+
when :backspace
|
|
745
|
+
editor_search_delete_character
|
|
746
|
+
else
|
|
747
|
+
false
|
|
748
|
+
end
|
|
749
|
+
else
|
|
750
|
+
case key_name
|
|
751
|
+
when :left
|
|
752
|
+
@editor_state.move_left
|
|
753
|
+
when :right
|
|
754
|
+
@editor_state.move_right
|
|
755
|
+
when :up
|
|
756
|
+
editor_move_up
|
|
757
|
+
when :down
|
|
758
|
+
editor_move_down
|
|
759
|
+
when :home
|
|
760
|
+
@editor_state.move_line_start
|
|
761
|
+
when :end
|
|
762
|
+
@editor_state.move_line_end
|
|
763
|
+
when :pageup
|
|
764
|
+
scroll_editor_up(editor_scroll_page_rows)
|
|
765
|
+
when :pagedown
|
|
766
|
+
scroll_editor_down(editor_scroll_page_rows)
|
|
767
|
+
else
|
|
768
|
+
false
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def editor_extending_selection
|
|
774
|
+
if @editor_state.multi_cursor?
|
|
775
|
+
@editor_state.extending_selections { yield }
|
|
776
|
+
else
|
|
777
|
+
@editor_state.selection_anchor ||= @editor_state.cursor
|
|
778
|
+
yield
|
|
779
|
+
end
|
|
780
|
+
true
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def editor_move_up
|
|
784
|
+
return @editor_state.move_up unless current_editor_soft_wrap?
|
|
785
|
+
|
|
786
|
+
line, column = @editor_state.cursor_line_and_column
|
|
787
|
+
text_width = current_editor_text_width
|
|
788
|
+
row_start = editor_visual_row_start_column(line, column, text_width)
|
|
789
|
+
if row_start.positive?
|
|
790
|
+
target_column = row_start - text_width + column - row_start
|
|
791
|
+
return @editor_state.set_cursor_line_and_column(line, target_column)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
@editor_state.move_up
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def editor_move_down
|
|
798
|
+
return @editor_state.move_down unless current_editor_soft_wrap?
|
|
799
|
+
|
|
800
|
+
line, column = @editor_state.cursor_line_and_column
|
|
801
|
+
text_width = current_editor_text_width
|
|
802
|
+
row_start = editor_visual_row_start_column(line, column, text_width)
|
|
803
|
+
next_start = row_start + text_width
|
|
804
|
+
current_line = @editor_state.lines[line].to_s
|
|
805
|
+
if next_start < current_line.length
|
|
806
|
+
target_column = [next_start + column - row_start, current_line.length].min
|
|
807
|
+
return @editor_state.set_cursor_line_and_column(line, target_column)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
@editor_state.move_down
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def current_editor_text_width
|
|
814
|
+
return @editor_text_width if @editor_text_width
|
|
815
|
+
|
|
816
|
+
content_width = [screen_width - 4, 1].max
|
|
817
|
+
editor_text_width(content_width)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def sync_editor_wrap_state(text_width = current_editor_text_width)
|
|
821
|
+
return unless @editor_state
|
|
822
|
+
|
|
823
|
+
@editor_text_width = text_width
|
|
824
|
+
@editor_state.viewport_column = 0 if current_editor_soft_wrap?
|
|
825
|
+
text_width
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def editor_selection_active?
|
|
829
|
+
@editor_state&.selection_active?
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def clear_editor_selection_before_edit
|
|
833
|
+
@editor_state&.clear_selection
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def delete_editor_selection
|
|
837
|
+
return false unless @editor_state.selection_ranges.any?
|
|
838
|
+
|
|
839
|
+
@editor_state.replace_selections("")
|
|
840
|
+
true
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def copy_editor_selection
|
|
844
|
+
text = @editor_state.selected_text
|
|
845
|
+
return false if text.empty?
|
|
846
|
+
|
|
847
|
+
@editor_state.push_kill(text)
|
|
848
|
+
@output_io.print("\e]52;c;#{Base64.strict_encode64(text)}\a")
|
|
849
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
850
|
+
@editor_state.clear_selection
|
|
851
|
+
@editor_state.status = "Copied selection"
|
|
852
|
+
true
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def cut_editor_selection
|
|
856
|
+
text = @editor_state.selected_text
|
|
857
|
+
return false if text.empty?
|
|
858
|
+
|
|
859
|
+
@editor_state.push_kill(text)
|
|
860
|
+
@editor_state.replace_selections("")
|
|
861
|
+
@editor_state.status = "Cut selection"
|
|
862
|
+
true
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def editor_search_active?
|
|
866
|
+
@editor_state&.search_active
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def editor_search_begin(direction = :forward)
|
|
870
|
+
@editor_state.begin_search(direction)
|
|
871
|
+
true
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def editor_search_append(text)
|
|
875
|
+
@editor_state.append_search(text)
|
|
876
|
+
true
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def editor_search_delete_character
|
|
880
|
+
@editor_state.delete_search_character
|
|
881
|
+
true
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def editor_search_confirm
|
|
885
|
+
@editor_state.confirm_search
|
|
886
|
+
true
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def editor_search_cancel
|
|
890
|
+
@editor_state.cancel_search
|
|
891
|
+
true
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
def editor_search_repeat(direction = nil)
|
|
895
|
+
direction ||= @editor_state.search_direction
|
|
896
|
+
@editor_state.repeat_search(direction)
|
|
897
|
+
true
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def editor_search_word_under_cursor(direction = :forward)
|
|
901
|
+
query = @editor_state.word_under_cursor
|
|
902
|
+
if query.empty?
|
|
903
|
+
@editor_state.status = "No word under cursor"
|
|
904
|
+
return true
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
@editor_state.repeat_search(direction, query)
|
|
908
|
+
true
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def editor_page_rows
|
|
912
|
+
[editor_visible_line_count, 1].max
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
def editor_scroll_page_rows
|
|
916
|
+
[editor_visible_line_count / 2, 1].max
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def editor_mouse_scroll_rows
|
|
920
|
+
1
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def enable_editor_mouse_reporting
|
|
924
|
+
return if @editor_mouse_reporting_enabled
|
|
925
|
+
|
|
926
|
+
@output_io.print("\e[?1003h\e[?1006h")
|
|
927
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
928
|
+
@editor_mouse_reporting_enabled = true
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def disable_editor_mouse_reporting(force: false)
|
|
932
|
+
return unless force || @editor_mouse_reporting_enabled
|
|
933
|
+
|
|
934
|
+
@output_io.print("\e[?1006l\e[?1003l")
|
|
935
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
936
|
+
@editor_mouse_reporting_enabled = false
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def scroll_editor_up(rows)
|
|
940
|
+
visible_count = editor_visible_line_count
|
|
941
|
+
@editor_state.viewport_row = [@editor_state.viewport_row - rows.to_i, 0].max
|
|
942
|
+
keep_editor_cursor_in_view(visible_count)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def scroll_editor_down(rows)
|
|
946
|
+
visible_count = editor_visible_line_count
|
|
947
|
+
last_top_row = if current_editor_soft_wrap?
|
|
948
|
+
[editor_visual_rows(current_editor_text_width).length - visible_count, 0].max
|
|
949
|
+
else
|
|
950
|
+
[@editor_state.lines.length - visible_count, 0].max
|
|
951
|
+
end
|
|
952
|
+
@editor_state.viewport_row = [@editor_state.viewport_row + rows.to_i, last_top_row].min
|
|
953
|
+
keep_editor_cursor_in_view(visible_count)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def keep_editor_cursor_in_view(visible_count)
|
|
957
|
+
line, column = @editor_state.cursor_line_and_column
|
|
958
|
+
if current_editor_soft_wrap?
|
|
959
|
+
text_width = current_editor_text_width
|
|
960
|
+
top_row = @editor_state.viewport_row
|
|
961
|
+
bottom_row = top_row + visible_count - 1
|
|
962
|
+
while editor_visual_row_for(*@editor_state.cursor_line_and_column, text_width) > bottom_row && @editor_state.cursor.positive?
|
|
963
|
+
editor_move_up
|
|
964
|
+
end
|
|
965
|
+
while editor_visual_row_for(*@editor_state.cursor_line_and_column, text_width) < top_row && @editor_state.cursor < @editor_state.buffer.length
|
|
966
|
+
editor_move_down
|
|
967
|
+
end
|
|
968
|
+
return true
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
top_line = @editor_state.viewport_row
|
|
972
|
+
bottom_line = top_line + visible_count - 1
|
|
973
|
+
|
|
974
|
+
if line < top_line
|
|
975
|
+
@editor_state.set_cursor_line_and_column(top_line, column)
|
|
976
|
+
elsif line > bottom_line
|
|
977
|
+
@editor_state.set_cursor_line_and_column(bottom_line, column)
|
|
978
|
+
end
|
|
979
|
+
true
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
def quit_editor(message = "Unsaved changes. Press Ctrl+Q again to discard.")
|
|
983
|
+
return false unless @editor_state
|
|
984
|
+
return close_editor unless @editor_state.dirty?
|
|
985
|
+
return close_editor if @editor_state.quit_confirmed
|
|
986
|
+
|
|
987
|
+
@editor_state.quit_confirmed = true
|
|
988
|
+
@editor_state.status = message
|
|
989
|
+
true
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
def save_editor
|
|
993
|
+
return false unless @editor_state
|
|
994
|
+
if @editor_state.readonly?
|
|
995
|
+
@editor_state.status = "Read-only diff"
|
|
996
|
+
return true
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
if @editor_state.file_changed_on_disk? && !@editor_state.overwrite_confirmed
|
|
1000
|
+
@editor_state.overwrite_confirmed = true
|
|
1001
|
+
@editor_state.status = "File changed on disk. Press Ctrl+S again to overwrite."
|
|
1002
|
+
return true
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
File.write(@editor_state.path, @editor_state.buffer)
|
|
1006
|
+
@editor_state.refresh_after_save(@editor_state.buffer)
|
|
1007
|
+
true
|
|
1008
|
+
rescue StandardError => e
|
|
1009
|
+
@editor_state.status = "Save failed: #{e.message}" if @editor_state
|
|
1010
|
+
false
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def printable_key?(key)
|
|
1014
|
+
key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
end
|