kward 0.71.0 → 0.73.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/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +93 -0
- 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 +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -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 +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -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/ansi.rb +62 -23
- 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/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -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 +559 -0
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -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 +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -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 +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -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 +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -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 +288 -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 +451 -57
- 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 +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- 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 +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- 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/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -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 +204 -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 +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -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 +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "../../scratchpad_runner"
|
|
3
|
+
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
|
+
module Kward
|
|
6
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
7
|
+
class PromptInterface
|
|
8
|
+
# Built-in composer file editor behavior.
|
|
9
|
+
module EditorController
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def editor_active?
|
|
13
|
+
!@editor_state.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def open_selected_file_in_editor(fallback_to_typed_path: false)
|
|
17
|
+
path = selected_file_open_path
|
|
18
|
+
if path
|
|
19
|
+
opened = open_editor(path)
|
|
20
|
+
add_history(history_file_open_command(path)) if opened
|
|
21
|
+
return opened
|
|
22
|
+
end
|
|
23
|
+
return false unless fallback_to_typed_path
|
|
24
|
+
|
|
25
|
+
open_typed_file_path_in_editor
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def open_typed_file_path_in_editor
|
|
29
|
+
file_open = active_file_open
|
|
30
|
+
return false unless file_open
|
|
31
|
+
if file_open[:query].empty?
|
|
32
|
+
@file_editor_open_status = "Type a file path after $"
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opened = open_editor(file_open[:query], allow_new: true)
|
|
37
|
+
add_history(history_file_open_command(file_open[:query])) if opened
|
|
38
|
+
opened
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def history_file_open_command(path)
|
|
42
|
+
full_path = File.expand_path(path.to_s, Dir.pwd)
|
|
43
|
+
relative_path = Pathname.new(full_path).relative_path_from(Pathname.new(File.expand_path(Dir.pwd))).to_s
|
|
44
|
+
"$#{relative_path}"
|
|
45
|
+
rescue ArgumentError
|
|
46
|
+
"$#{path}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open_scratchpad(language = :text, content: "")
|
|
50
|
+
language = normalize_scratchpad_language(language)
|
|
51
|
+
@editor_state = EditorState.new(
|
|
52
|
+
path: scratchpad_display_path(language),
|
|
53
|
+
display_path: scratchpad_display_path(language),
|
|
54
|
+
content: content.to_s,
|
|
55
|
+
new_file: true,
|
|
56
|
+
editor_mode: current_editor_mode,
|
|
57
|
+
virtual: true,
|
|
58
|
+
language: language
|
|
59
|
+
)
|
|
60
|
+
@editor_state.status = scratchpad_status_text(language)
|
|
61
|
+
@prompt_label = "Edit>"
|
|
62
|
+
self.composer_input = ""
|
|
63
|
+
self.composer_cursor = 0
|
|
64
|
+
@composer.clear_attachments
|
|
65
|
+
@pending_keys.clear
|
|
66
|
+
@file_overlay_dismissed_token = nil
|
|
67
|
+
@file_open_dismissed_token = nil
|
|
68
|
+
@asking = true
|
|
69
|
+
set_editor_bar_cursor_locked if current_editor_bar_cursor?
|
|
70
|
+
enable_editor_mouse_reporting
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def open_editor(path, allow_new: false, base_dir: Dir.pwd, restrict_to_workspace: true)
|
|
75
|
+
full_path = File.expand_path(path.to_s, base_dir)
|
|
76
|
+
root = File.expand_path(Dir.pwd)
|
|
77
|
+
if restrict_to_workspace && !(full_path == root || full_path.start_with?("#{root}/"))
|
|
78
|
+
@file_editor_open_status = "Cannot edit file outside workspace"
|
|
79
|
+
return false
|
|
80
|
+
end
|
|
81
|
+
if File.exist?(full_path) && !File.file?(full_path)
|
|
82
|
+
@file_editor_open_status = "Cannot edit non-file path: #{path}"
|
|
83
|
+
return false
|
|
84
|
+
end
|
|
85
|
+
unless File.exist?(full_path)
|
|
86
|
+
unless allow_new
|
|
87
|
+
@file_editor_open_status = "Cannot edit missing file: #{path}"
|
|
88
|
+
return false
|
|
89
|
+
end
|
|
90
|
+
parent = File.dirname(full_path)
|
|
91
|
+
unless Dir.exist?(parent)
|
|
92
|
+
@file_editor_open_status = "Cannot create file; parent directory is missing"
|
|
93
|
+
return false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@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)
|
|
98
|
+
@prompt_label = "Edit>"
|
|
99
|
+
self.composer_input = ""
|
|
100
|
+
self.composer_cursor = 0
|
|
101
|
+
@composer.clear_attachments
|
|
102
|
+
@pending_keys.clear
|
|
103
|
+
@file_overlay_dismissed_token = nil
|
|
104
|
+
@file_open_dismissed_token = nil
|
|
105
|
+
@asking = true
|
|
106
|
+
set_editor_bar_cursor_locked if current_editor_bar_cursor?
|
|
107
|
+
enable_editor_mouse_reporting
|
|
108
|
+
true
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
@file_editor_open_status = "Cannot edit #{path}: #{e.message}"
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def open_diff_viewer(path, content)
|
|
115
|
+
@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)
|
|
116
|
+
@prompt_label = "Diff>"
|
|
117
|
+
self.composer_input = ""
|
|
118
|
+
self.composer_cursor = 0
|
|
119
|
+
@composer.clear_attachments
|
|
120
|
+
@pending_keys.clear
|
|
121
|
+
@asking = true
|
|
122
|
+
enable_editor_mouse_reporting
|
|
123
|
+
true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def current_editor_mode
|
|
127
|
+
return normalize_editor_mode(@editor_mode_source.call) if @editor_mode_source.respond_to?(:call)
|
|
128
|
+
|
|
129
|
+
@editor_mode
|
|
130
|
+
rescue StandardError
|
|
131
|
+
@editor_mode
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def current_editor_soft_wrap?
|
|
135
|
+
return @editor_soft_wrap_source.call != false if @editor_soft_wrap_source.respond_to?(:call)
|
|
136
|
+
|
|
137
|
+
@editor_soft_wrap != false
|
|
138
|
+
rescue StandardError
|
|
139
|
+
@editor_soft_wrap != false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def current_editor_bar_cursor?
|
|
143
|
+
return @editor_bar_cursor_source.call != false if @editor_bar_cursor_source.respond_to?(:call)
|
|
144
|
+
|
|
145
|
+
@editor_bar_cursor != false
|
|
146
|
+
rescue StandardError
|
|
147
|
+
@editor_bar_cursor != false
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def current_editor_line_numbers
|
|
151
|
+
return normalize_editor_line_numbers(@editor_line_numbers_source.call) if @editor_line_numbers_source.respond_to?(:call)
|
|
152
|
+
|
|
153
|
+
normalize_editor_line_numbers(@editor_line_numbers)
|
|
154
|
+
rescue StandardError
|
|
155
|
+
normalize_editor_line_numbers(@editor_line_numbers)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def close_editor
|
|
159
|
+
disable_editor_mouse_reporting(force: true)
|
|
160
|
+
restore_editor_cursor_shape_locked
|
|
161
|
+
@editor_text_width = nil
|
|
162
|
+
@editor_save_as_active = false
|
|
163
|
+
@editor_save_as_buffer = ""
|
|
164
|
+
@editor_state = nil
|
|
165
|
+
@prompt_label = "You>"
|
|
166
|
+
self.composer_input = ""
|
|
167
|
+
self.composer_cursor = 0
|
|
168
|
+
restore_project_browser_after_editor_close
|
|
169
|
+
@asking = true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def handle_editor_key(key)
|
|
173
|
+
return if key.nil?
|
|
174
|
+
mouse_result = handle_editor_mouse_key(key)
|
|
175
|
+
return mouse_result unless mouse_result == false
|
|
176
|
+
return handle_editor_save_as_key(key) if @editor_save_as_active
|
|
177
|
+
return handle_readonly_editor_key(key) if @editor_state&.readonly?
|
|
178
|
+
return handle_vibe_key(key) if @editor_state&.vibe?
|
|
179
|
+
return handle_emacs_key(key) if @editor_state&.emacs?
|
|
180
|
+
return handle_modern_key(key) if @editor_state&.modern?
|
|
181
|
+
return if handle_editor_bracketed_paste_key(key)
|
|
182
|
+
|
|
183
|
+
csi_result = handle_editor_csi_u_key(key)
|
|
184
|
+
return csi_result unless csi_result == false
|
|
185
|
+
|
|
186
|
+
shift_result = handle_editor_shift_navigation_key(key)
|
|
187
|
+
return shift_result unless shift_result == false
|
|
188
|
+
|
|
189
|
+
binding_result = handle_editor_key_binding(key)
|
|
190
|
+
return binding_result unless binding_result == false
|
|
191
|
+
|
|
192
|
+
editor_tab_result = handle_editor_tab_key(key)
|
|
193
|
+
return editor_tab_result unless editor_tab_result == false
|
|
194
|
+
|
|
195
|
+
tab_result = handle_tab_key_binding(key)
|
|
196
|
+
return tab_result unless tab_result == false
|
|
197
|
+
|
|
198
|
+
return true if handle_bundled_key(key) { |token| handle_editor_key(token) }
|
|
199
|
+
|
|
200
|
+
case key
|
|
201
|
+
when "\n", "\r"
|
|
202
|
+
return editor_search_confirm if editor_search_active?
|
|
203
|
+
clear_editor_selection_before_edit
|
|
204
|
+
editor_insert_newline
|
|
205
|
+
when "\t"
|
|
206
|
+
editor_insert_tab unless editor_search_active?
|
|
207
|
+
when "\b", "\x7F"
|
|
208
|
+
editor_search_active? ? editor_search_delete_character : delete_editor_selection || editor_delete_before_cursor
|
|
209
|
+
when TerminalKeys::CTRL_C
|
|
210
|
+
return editor_search_cancel if editor_search_active?
|
|
211
|
+
when "\e"
|
|
212
|
+
return editor_search_cancel if editor_search_active?
|
|
213
|
+
return @editor_state.clear_selection if @editor_state.selection_active?
|
|
214
|
+
when TerminalKeys::CTRL_Q
|
|
215
|
+
quit_editor
|
|
216
|
+
when TerminalKeys::CTRL_S
|
|
217
|
+
save_editor
|
|
218
|
+
when "/"
|
|
219
|
+
clear_editor_selection_before_edit unless editor_search_active?
|
|
220
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
221
|
+
else
|
|
222
|
+
key_name = key_name_for(key)
|
|
223
|
+
named_result = handle_editor_named_key(key_name) if key_name
|
|
224
|
+
return named_result unless named_result == false || named_result.nil?
|
|
225
|
+
|
|
226
|
+
if editor_search_active?
|
|
227
|
+
editor_search_append(key) if printable_key?(key)
|
|
228
|
+
elsif printable_key?(key)
|
|
229
|
+
editor_insert_printable(key)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def handle_editor_csi_u_key(key)
|
|
235
|
+
sequence = parse_csi_u_key(key)
|
|
236
|
+
return false unless sequence
|
|
237
|
+
|
|
238
|
+
handle_parsed_editor_csi_u_key(sequence)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def handle_parsed_editor_csi_u_key(sequence)
|
|
242
|
+
code = sequence[:code]
|
|
243
|
+
modifier = sequence[:modifier]
|
|
244
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
245
|
+
|
|
246
|
+
binding_result = handle_editor_modified_csi_u_key(code, modifier)
|
|
247
|
+
return binding_result unless binding_result == false
|
|
248
|
+
text = csi_u_printable_text(sequence)
|
|
249
|
+
return editor_insert_csi_u_text(text) if text
|
|
250
|
+
return true if csi_u_text_field?(sequence)
|
|
251
|
+
|
|
252
|
+
case code
|
|
253
|
+
when 9
|
|
254
|
+
return false if editor_search_active?
|
|
255
|
+
return false if ctrl_modifier?(modifier) || alt_modifier?(modifier) || super_modifier?(modifier)
|
|
256
|
+
|
|
257
|
+
shift_modifier?(modifier) ? editor_outdent_tab : editor_insert_tab
|
|
258
|
+
when 13
|
|
259
|
+
clear_editor_selection_before_edit unless editor_search_active?
|
|
260
|
+
editor_search_active? ? editor_search_confirm : editor_insert_newline
|
|
261
|
+
when 27
|
|
262
|
+
editor_search_active? ? editor_search_cancel : @editor_state.clear_selection
|
|
263
|
+
when 8, 127
|
|
264
|
+
editor_search_active? ? editor_search_delete_character : delete_editor_selection || editor_delete_before_cursor
|
|
265
|
+
nil
|
|
266
|
+
when 4
|
|
267
|
+
delete_editor_selection || @editor_state.delete_at_cursor unless editor_search_active?
|
|
268
|
+
nil
|
|
269
|
+
else
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def editor_insert_csi_u_text(text)
|
|
275
|
+
if editor_search_active?
|
|
276
|
+
editor_search_append(text)
|
|
277
|
+
else
|
|
278
|
+
editor_insert_printable(text)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def handle_editor_mouse_key(key)
|
|
283
|
+
event = parse_editor_mouse_key(key)
|
|
284
|
+
return false unless event
|
|
285
|
+
|
|
286
|
+
queue_pending_keys(event[:remaining]) unless event[:remaining].empty?
|
|
287
|
+
case event[:code]
|
|
288
|
+
when 64
|
|
289
|
+
scroll_editor_up(editor_mouse_scroll_rows)
|
|
290
|
+
when 65
|
|
291
|
+
scroll_editor_down(editor_mouse_scroll_rows)
|
|
292
|
+
else
|
|
293
|
+
if event[:drag]
|
|
294
|
+
handle_editor_mouse_drag(event)
|
|
295
|
+
elsif event[:button].zero?
|
|
296
|
+
event[:release] ? finish_editor_mouse_drag : handle_editor_mouse_press(event)
|
|
297
|
+
else
|
|
298
|
+
true
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def parse_editor_mouse_key(key)
|
|
304
|
+
parse_sgr_mouse_event(key)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def handle_editor_mouse_press(event)
|
|
308
|
+
position = editor_position_for_mouse_event(event)
|
|
309
|
+
return true unless position
|
|
310
|
+
|
|
311
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
312
|
+
click_count = editor_mouse_click_count(event, now)
|
|
313
|
+
case click_count
|
|
314
|
+
when 3..Float::INFINITY
|
|
315
|
+
range = select_editor_line_at(position[:line])
|
|
316
|
+
@editor_mouse_drag_anchor = range[0]
|
|
317
|
+
@editor_mouse_dragging = true
|
|
318
|
+
when 2
|
|
319
|
+
range = select_editor_word_at(position[:offset])
|
|
320
|
+
if range
|
|
321
|
+
@editor_mouse_drag_anchor = range[0]
|
|
322
|
+
@editor_mouse_dragging = true
|
|
323
|
+
else
|
|
324
|
+
finish_editor_mouse_drag
|
|
325
|
+
end
|
|
326
|
+
else
|
|
327
|
+
@editor_state.clear_selection
|
|
328
|
+
@editor_state.cursor = position[:offset]
|
|
329
|
+
@editor_mouse_drag_anchor = position[:offset]
|
|
330
|
+
@editor_mouse_dragging = true
|
|
331
|
+
end
|
|
332
|
+
@editor_last_click = { time: now, column: event[:column], row: event[:row], count: click_count }
|
|
333
|
+
true
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def handle_editor_mouse_drag(event)
|
|
337
|
+
return true unless @editor_mouse_dragging
|
|
338
|
+
|
|
339
|
+
position = editor_drag_position_for_mouse_event(event)
|
|
340
|
+
return true unless position
|
|
341
|
+
|
|
342
|
+
@editor_state.selection_anchor = @editor_mouse_drag_anchor
|
|
343
|
+
@editor_state.cursor = position[:offset]
|
|
344
|
+
true
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def finish_editor_mouse_drag
|
|
348
|
+
@editor_mouse_dragging = false
|
|
349
|
+
@editor_mouse_drag_anchor = nil
|
|
350
|
+
true
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def editor_mouse_click_count(event, now)
|
|
354
|
+
return 1 unless editor_repeated_click?(event, now)
|
|
355
|
+
|
|
356
|
+
@editor_last_click[:count].to_i + 1
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def editor_repeated_click?(event, now)
|
|
360
|
+
return false unless @editor_last_click
|
|
361
|
+
return false unless now - @editor_last_click[:time] <= 0.5
|
|
362
|
+
|
|
363
|
+
(@editor_last_click[:column] - event[:column]).abs <= 1 && (@editor_last_click[:row] - event[:row]).abs <= 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def select_editor_word_at(offset)
|
|
367
|
+
range = @editor_state.word_range_at(offset)
|
|
368
|
+
return @editor_state.clear_selection unless range
|
|
369
|
+
|
|
370
|
+
@editor_state.selection_anchor = range[0]
|
|
371
|
+
@editor_state.cursor = range[1]
|
|
372
|
+
range
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def select_editor_line_at(line_index)
|
|
376
|
+
range = @editor_state.line_range(line_index)
|
|
377
|
+
@editor_state.selection_anchor = range[0]
|
|
378
|
+
@editor_state.cursor = range[1]
|
|
379
|
+
range
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def editor_drag_position_for_mouse_event(event)
|
|
383
|
+
scroll_editor_horizontally_for_drag(event)
|
|
384
|
+
top = editor_mouse_content_top_row
|
|
385
|
+
bottom = top + editor_visible_line_count - 1
|
|
386
|
+
if event[:row] < top
|
|
387
|
+
scroll_editor_up(editor_mouse_scroll_rows)
|
|
388
|
+
return editor_edge_position_for_mouse_event(event, @editor_state.viewport_row)
|
|
389
|
+
elsif event[:row] > bottom
|
|
390
|
+
scroll_editor_down(editor_mouse_scroll_rows)
|
|
391
|
+
return editor_edge_position_for_mouse_event(event, @editor_state.viewport_row + editor_visible_line_count - 1)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
editor_position_for_mouse_event(event)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def editor_position_for_mouse_event(event)
|
|
398
|
+
row_offset = event[:row] - editor_mouse_content_top_row
|
|
399
|
+
return nil if row_offset.negative? || row_offset >= editor_visible_line_count
|
|
400
|
+
|
|
401
|
+
if current_editor_soft_wrap?
|
|
402
|
+
editor_wrapped_position_for_mouse(event, row_offset)
|
|
403
|
+
else
|
|
404
|
+
line_index = @editor_state.viewport_row + row_offset
|
|
405
|
+
editor_position_for_line_and_column(line_index, editor_mouse_column_for_event(event))
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def editor_wrapped_position_for_mouse(event, row_offset)
|
|
410
|
+
editor_wrapped_position_for_visual_row(event, @editor_state.viewport_row + row_offset)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def editor_edge_position_for_mouse_event(event, row_index)
|
|
414
|
+
if current_editor_soft_wrap?
|
|
415
|
+
editor_wrapped_position_for_visual_row(event, row_index)
|
|
416
|
+
else
|
|
417
|
+
editor_position_for_line_and_column(row_index, editor_mouse_column_for_event(event))
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def editor_wrapped_position_for_visual_row(event, row_index)
|
|
422
|
+
visual_row = editor_visual_rows(current_editor_text_width)[row_index]
|
|
423
|
+
return nil unless visual_row
|
|
424
|
+
|
|
425
|
+
column = visual_row[:column_offset] + editor_mouse_column_for_event(event)
|
|
426
|
+
editor_position_for_line_and_column(visual_row[:line_index], column)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def editor_position_for_line_and_column(line_index, column)
|
|
430
|
+
lines = @editor_state.lines
|
|
431
|
+
line_index = [[line_index.to_i, 0].max, lines.length - 1].min
|
|
432
|
+
column = [[column.to_i, 0].max, lines[line_index].to_s.length].min
|
|
433
|
+
@editor_state.set_cursor_line_and_column(line_index, column)
|
|
434
|
+
{ line: line_index, column: column, offset: @editor_state.cursor }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def editor_bottom_mouse_line_index
|
|
438
|
+
[@editor_state.viewport_row + editor_visible_line_count - 1, @editor_state.lines.length - 1].min
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def scroll_editor_horizontally_for_drag(event)
|
|
442
|
+
return if current_editor_soft_wrap?
|
|
443
|
+
|
|
444
|
+
if event[:column] < editor_mouse_text_left_column
|
|
445
|
+
@editor_state.viewport_column = [@editor_state.viewport_column.to_i - editor_mouse_scroll_rows, 0].max
|
|
446
|
+
elsif event[:column] > editor_mouse_text_right_column
|
|
447
|
+
@editor_state.viewport_column = @editor_state.viewport_column.to_i + editor_mouse_scroll_rows
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def editor_mouse_column_for_event(event)
|
|
452
|
+
column = [event[:column] - editor_mouse_text_left_column, 0].max
|
|
453
|
+
current_editor_soft_wrap? ? column : column + @editor_state.viewport_column.to_i
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def editor_mouse_text_left_column
|
|
457
|
+
3 + editor_line_number_gutter_width
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def editor_mouse_text_right_column
|
|
461
|
+
editor_mouse_text_left_column + current_editor_text_width - 1
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def editor_mouse_content_top_row
|
|
465
|
+
3
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def handle_editor_shift_navigation_key(key)
|
|
469
|
+
return false if editor_search_active?
|
|
470
|
+
|
|
471
|
+
case key
|
|
472
|
+
when *TerminalKeys::SHIFT_LEFT
|
|
473
|
+
editor_extending_selection { @editor_state.move_left }
|
|
474
|
+
when *TerminalKeys::SHIFT_RIGHT
|
|
475
|
+
editor_extending_selection { @editor_state.move_right }
|
|
476
|
+
when *TerminalKeys::SHIFT_UP
|
|
477
|
+
editor_extending_selection { editor_move_up }
|
|
478
|
+
when *TerminalKeys::SHIFT_DOWN
|
|
479
|
+
editor_extending_selection { editor_move_down }
|
|
480
|
+
else
|
|
481
|
+
false
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def handle_editor_key_binding(key)
|
|
486
|
+
case key
|
|
487
|
+
when TerminalKeys::CTRL_A
|
|
488
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
489
|
+
when TerminalKeys::CTRL_B
|
|
490
|
+
@editor_state.move_left unless editor_search_active?
|
|
491
|
+
when TerminalKeys::CTRL_D
|
|
492
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
493
|
+
when TerminalKeys::CTRL_E
|
|
494
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
495
|
+
when TerminalKeys::CTRL_F
|
|
496
|
+
@editor_state.move_right unless editor_search_active?
|
|
497
|
+
when TerminalKeys::CTRL_SPACE
|
|
498
|
+
@editor_state.begin_selection unless editor_search_active?
|
|
499
|
+
when TerminalKeys::CTRL_K
|
|
500
|
+
@editor_state.kill_line_after_cursor unless editor_search_active?
|
|
501
|
+
when TerminalKeys::CTRL_N
|
|
502
|
+
editor_move_down unless editor_search_active?
|
|
503
|
+
when TerminalKeys::CTRL_P
|
|
504
|
+
editor_move_up unless editor_search_active?
|
|
505
|
+
when TerminalKeys::CTRL_U
|
|
506
|
+
@editor_state.kill_line_before_cursor unless editor_search_active?
|
|
507
|
+
when TerminalKeys::CTRL_W
|
|
508
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
509
|
+
when TerminalKeys::CTRL_Y
|
|
510
|
+
editor_selection_active? ? copy_editor_selection : @editor_state.yank_kill_buffer unless editor_search_active?
|
|
511
|
+
when *TerminalKeys::LEFT
|
|
512
|
+
@editor_state.move_left unless editor_search_active?
|
|
513
|
+
when *TerminalKeys::RIGHT
|
|
514
|
+
@editor_state.move_right unless editor_search_active?
|
|
515
|
+
when *TerminalKeys::HOME
|
|
516
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
517
|
+
when *TerminalKeys::END_KEY
|
|
518
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
519
|
+
when *TerminalKeys::DELETE
|
|
520
|
+
delete_editor_selection || @editor_state.delete_at_cursor unless editor_search_active?
|
|
521
|
+
when "\eb", "\eB"
|
|
522
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
523
|
+
when "\ef", "\eF"
|
|
524
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
525
|
+
when "\ed", "\eD"
|
|
526
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
527
|
+
when "\e\b", "\e\x7F"
|
|
528
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
529
|
+
else
|
|
530
|
+
handle_editor_modified_ansi_key(key) || false
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def handle_editor_modified_csi_u_key(code, modifier)
|
|
535
|
+
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
536
|
+
|
|
537
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
538
|
+
if ctrl_modifier?(modifier)
|
|
539
|
+
case normalized_code
|
|
540
|
+
when 32
|
|
541
|
+
@editor_state.begin_selection unless editor_search_active?
|
|
542
|
+
when 97
|
|
543
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
544
|
+
when 98
|
|
545
|
+
@editor_state.move_left unless editor_search_active?
|
|
546
|
+
when 99
|
|
547
|
+
editor_search_cancel if editor_search_active?
|
|
548
|
+
when 100
|
|
549
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
550
|
+
when 101
|
|
551
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
552
|
+
when 102
|
|
553
|
+
@editor_state.move_right unless editor_search_active?
|
|
554
|
+
when 107
|
|
555
|
+
@editor_state.kill_line_after_cursor unless editor_search_active?
|
|
556
|
+
when 110
|
|
557
|
+
editor_move_down unless editor_search_active?
|
|
558
|
+
when 112
|
|
559
|
+
editor_move_up unless editor_search_active?
|
|
560
|
+
when 113
|
|
561
|
+
quit_editor
|
|
562
|
+
when 115
|
|
563
|
+
save_editor
|
|
564
|
+
when 117
|
|
565
|
+
@editor_state.kill_line_before_cursor unless editor_search_active?
|
|
566
|
+
when 119
|
|
567
|
+
@editor_state.delete_word_before_cursor unless editor_search_active?
|
|
568
|
+
when 121
|
|
569
|
+
editor_selection_active? ? copy_editor_selection : @editor_state.yank_kill_buffer unless editor_search_active?
|
|
570
|
+
else
|
|
571
|
+
false
|
|
572
|
+
end
|
|
573
|
+
elsif alt_modifier?(modifier)
|
|
574
|
+
case normalized_code
|
|
575
|
+
when 98
|
|
576
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
577
|
+
when 100
|
|
578
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
579
|
+
when 102
|
|
580
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
581
|
+
else
|
|
582
|
+
false
|
|
583
|
+
end
|
|
584
|
+
else
|
|
585
|
+
false
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def handle_editor_modified_ansi_key(key)
|
|
590
|
+
sequence = parse_modified_ansi_key(key)
|
|
591
|
+
return false unless sequence
|
|
592
|
+
|
|
593
|
+
case sequence[:type]
|
|
594
|
+
when :cursor
|
|
595
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
596
|
+
|
|
597
|
+
case sequence[:final]
|
|
598
|
+
when "C"
|
|
599
|
+
@editor_state.move_to_next_word unless editor_search_active?
|
|
600
|
+
when "D"
|
|
601
|
+
@editor_state.move_to_previous_word unless editor_search_active?
|
|
602
|
+
when "F"
|
|
603
|
+
@editor_state.move_line_end unless editor_search_active?
|
|
604
|
+
when "H"
|
|
605
|
+
@editor_state.move_line_start unless editor_search_active?
|
|
606
|
+
else
|
|
607
|
+
false
|
|
608
|
+
end
|
|
609
|
+
when :delete
|
|
610
|
+
if alt_modifier?(sequence[:modifier])
|
|
611
|
+
@editor_state.delete_word_after_cursor unless editor_search_active?
|
|
612
|
+
else
|
|
613
|
+
@editor_state.delete_at_cursor unless editor_search_active?
|
|
614
|
+
end
|
|
615
|
+
else
|
|
616
|
+
false
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def handle_editor_bracketed_paste_key(key)
|
|
621
|
+
handle_bracketed_paste(key) do |content|
|
|
622
|
+
@editor_state.insert(content) unless editor_search_active?
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def ctrl_code(code)
|
|
627
|
+
value = code.to_i
|
|
628
|
+
return value if value < 32
|
|
629
|
+
|
|
630
|
+
value.chr.downcase.ord
|
|
631
|
+
rescue StandardError
|
|
632
|
+
code
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def handle_editor_named_key(key_name)
|
|
636
|
+
return false unless key_name
|
|
637
|
+
|
|
638
|
+
if editor_search_active?
|
|
639
|
+
case key_name
|
|
640
|
+
when :return, :enter
|
|
641
|
+
editor_search_confirm
|
|
642
|
+
when :backspace
|
|
643
|
+
editor_search_delete_character
|
|
644
|
+
else
|
|
645
|
+
false
|
|
646
|
+
end
|
|
647
|
+
else
|
|
648
|
+
case key_name
|
|
649
|
+
when :return, :enter
|
|
650
|
+
editor_insert_newline
|
|
651
|
+
when :backspace
|
|
652
|
+
delete_editor_selection || editor_delete_before_cursor
|
|
653
|
+
when :delete
|
|
654
|
+
delete_editor_selection || @editor_state.delete_at_cursor
|
|
655
|
+
when :left
|
|
656
|
+
@editor_state.move_left
|
|
657
|
+
when :right
|
|
658
|
+
@editor_state.move_right
|
|
659
|
+
when :up
|
|
660
|
+
editor_move_up
|
|
661
|
+
when :down
|
|
662
|
+
editor_move_down
|
|
663
|
+
when :home
|
|
664
|
+
@editor_state.move_line_start
|
|
665
|
+
when :end
|
|
666
|
+
@editor_state.move_line_end
|
|
667
|
+
when :pageup
|
|
668
|
+
scroll_editor_up(editor_scroll_page_rows)
|
|
669
|
+
when :pagedown
|
|
670
|
+
scroll_editor_down(editor_scroll_page_rows)
|
|
671
|
+
else
|
|
672
|
+
false
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def handle_readonly_editor_key(key)
|
|
678
|
+
return if handle_readonly_bracketed_paste_key(key)
|
|
679
|
+
|
|
680
|
+
return true if handle_bundled_key(key) { |token| handle_readonly_editor_key(token) }
|
|
681
|
+
|
|
682
|
+
key_name = key_name_for(key)
|
|
683
|
+
named_result = handle_readonly_named_key(key_name) if key_name
|
|
684
|
+
return named_result unless named_result == false || named_result.nil?
|
|
685
|
+
|
|
686
|
+
case key
|
|
687
|
+
when TerminalKeys::CTRL_Q
|
|
688
|
+
close_editor
|
|
689
|
+
when TerminalKeys::CTRL_F
|
|
690
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
691
|
+
when TerminalKeys::CTRL_C
|
|
692
|
+
editor_search_active? ? editor_search_cancel : copy_editor_selection
|
|
693
|
+
when "/"
|
|
694
|
+
editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
695
|
+
when "\b", "\x7F"
|
|
696
|
+
editor_search_delete_character if editor_search_active?
|
|
697
|
+
when "\n", "\r"
|
|
698
|
+
editor_search_confirm if editor_search_active?
|
|
699
|
+
when "\e"
|
|
700
|
+
editor_search_active? ? editor_search_cancel : close_editor
|
|
701
|
+
else
|
|
702
|
+
csi_result = handle_readonly_csi_u_key(key)
|
|
703
|
+
return csi_result unless csi_result == false
|
|
704
|
+
|
|
705
|
+
if editor_search_active?
|
|
706
|
+
editor_search_append(key) if printable_key?(key)
|
|
707
|
+
elsif printable_key?(key)
|
|
708
|
+
@editor_state.status = "Read-only diff"
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def handle_readonly_csi_u_key(key)
|
|
714
|
+
sequence = parse_csi_u_key(key)
|
|
715
|
+
return false unless sequence
|
|
716
|
+
|
|
717
|
+
code = sequence[:code]
|
|
718
|
+
modifier = sequence[:modifier]
|
|
719
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
720
|
+
|
|
721
|
+
if ctrl_modifier?(modifier) && ctrl_code(code) == 102
|
|
722
|
+
return editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
if (ctrl_modifier?(modifier) || super_modifier?(modifier)) && ctrl_code(code) == 99
|
|
726
|
+
return editor_search_active? ? editor_search_cancel : copy_editor_selection
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
case code
|
|
730
|
+
when 13
|
|
731
|
+
editor_search_confirm if editor_search_active?
|
|
732
|
+
when 27
|
|
733
|
+
editor_search_active? ? editor_search_cancel : close_editor
|
|
734
|
+
when 8, 127
|
|
735
|
+
editor_search_delete_character if editor_search_active?
|
|
736
|
+
else
|
|
737
|
+
return false unless editor_search_active?
|
|
738
|
+
|
|
739
|
+
text = csi_u_printable_text(sequence)
|
|
740
|
+
return true if text.nil? && csi_u_text_field?(sequence)
|
|
741
|
+
return false unless text
|
|
742
|
+
|
|
743
|
+
editor_search_append(text)
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def handle_readonly_bracketed_paste_key(key)
|
|
748
|
+
handle_bracketed_paste(key) do |_content|
|
|
749
|
+
@editor_state.status = "Read-only diff" unless editor_search_active?
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def handle_readonly_named_key(key_name)
|
|
754
|
+
return false unless key_name
|
|
755
|
+
|
|
756
|
+
if editor_search_active?
|
|
757
|
+
case key_name
|
|
758
|
+
when :return, :enter
|
|
759
|
+
editor_search_confirm
|
|
760
|
+
when :backspace
|
|
761
|
+
editor_search_delete_character
|
|
762
|
+
else
|
|
763
|
+
false
|
|
764
|
+
end
|
|
765
|
+
else
|
|
766
|
+
case key_name
|
|
767
|
+
when :left
|
|
768
|
+
@editor_state.move_left
|
|
769
|
+
when :right
|
|
770
|
+
@editor_state.move_right
|
|
771
|
+
when :up
|
|
772
|
+
editor_move_up
|
|
773
|
+
when :down
|
|
774
|
+
editor_move_down
|
|
775
|
+
when :home
|
|
776
|
+
@editor_state.move_line_start
|
|
777
|
+
when :end
|
|
778
|
+
@editor_state.move_line_end
|
|
779
|
+
when :pageup
|
|
780
|
+
scroll_editor_up(editor_scroll_page_rows)
|
|
781
|
+
when :pagedown
|
|
782
|
+
scroll_editor_down(editor_scroll_page_rows)
|
|
783
|
+
else
|
|
784
|
+
false
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def editor_extending_selection
|
|
790
|
+
if @editor_state.multi_cursor?
|
|
791
|
+
@editor_state.extending_selections { yield }
|
|
792
|
+
else
|
|
793
|
+
@editor_state.selection_anchor ||= @editor_state.cursor
|
|
794
|
+
yield
|
|
795
|
+
end
|
|
796
|
+
true
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def editor_move_up
|
|
800
|
+
return @editor_state.move_up unless current_editor_soft_wrap?
|
|
801
|
+
|
|
802
|
+
line, column = @editor_state.cursor_line_and_column
|
|
803
|
+
text_width = current_editor_text_width
|
|
804
|
+
row_start = editor_visual_row_start_column(line, column, text_width)
|
|
805
|
+
visual_column = column - row_start
|
|
806
|
+
if row_start.positive?
|
|
807
|
+
target_column = row_start - text_width + visual_column
|
|
808
|
+
return @editor_state.set_cursor_line_and_column(line, target_column)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
return @editor_state.move_up if line.zero?
|
|
812
|
+
|
|
813
|
+
previous_line = @editor_state.lines[line - 1].to_s
|
|
814
|
+
previous_row_start = editor_last_visual_row_start_column(previous_line, text_width)
|
|
815
|
+
target_column = [previous_row_start + visual_column, previous_line.length].min
|
|
816
|
+
@editor_state.set_cursor_line_and_column(line - 1, target_column)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def editor_move_down
|
|
820
|
+
return @editor_state.move_down unless current_editor_soft_wrap?
|
|
821
|
+
|
|
822
|
+
line, column = @editor_state.cursor_line_and_column
|
|
823
|
+
text_width = current_editor_text_width
|
|
824
|
+
row_start = editor_visual_row_start_column(line, column, text_width)
|
|
825
|
+
visual_column = column - row_start
|
|
826
|
+
next_start = row_start + text_width
|
|
827
|
+
current_line = @editor_state.lines[line].to_s
|
|
828
|
+
if next_start < current_line.length
|
|
829
|
+
target_column = [next_start + visual_column, current_line.length].min
|
|
830
|
+
return @editor_state.set_cursor_line_and_column(line, target_column)
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
return @editor_state.move_down if line >= @editor_state.lines.length - 1
|
|
834
|
+
|
|
835
|
+
next_line = @editor_state.lines[line + 1].to_s
|
|
836
|
+
target_column = [visual_column, next_line.length].min
|
|
837
|
+
@editor_state.set_cursor_line_and_column(line + 1, target_column)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def editor_last_visual_row_start_column(line, text_width)
|
|
841
|
+
length = line.to_s.length
|
|
842
|
+
return 0 if length.zero?
|
|
843
|
+
|
|
844
|
+
((length - 1) / text_width) * text_width
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def current_editor_text_width
|
|
848
|
+
return @editor_text_width if @editor_text_width
|
|
849
|
+
|
|
850
|
+
content_width = [screen_width - 4, 1].max
|
|
851
|
+
editor_text_width(content_width)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def sync_editor_wrap_state(text_width = current_editor_text_width)
|
|
855
|
+
return unless @editor_state
|
|
856
|
+
|
|
857
|
+
@editor_text_width = text_width
|
|
858
|
+
@editor_state.viewport_column = 0 if current_editor_soft_wrap?
|
|
859
|
+
text_width
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def editor_selection_active?
|
|
863
|
+
@editor_state&.selection_active?
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def clear_editor_selection_before_edit
|
|
867
|
+
@editor_state&.clear_selection
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def delete_editor_selection
|
|
871
|
+
return false unless @editor_state.selection_ranges.any?
|
|
872
|
+
|
|
873
|
+
@editor_state.replace_selections("")
|
|
874
|
+
true
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
def copy_editor_selection
|
|
878
|
+
text = @editor_state.selected_text
|
|
879
|
+
return false if text.empty?
|
|
880
|
+
|
|
881
|
+
@editor_state.push_kill(text)
|
|
882
|
+
@output_io.print(TerminalSequences.osc52(text))
|
|
883
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
884
|
+
@editor_state.clear_selection
|
|
885
|
+
@editor_state.status = "Copied selection"
|
|
886
|
+
true
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def cut_editor_selection
|
|
890
|
+
text = @editor_state.selected_text
|
|
891
|
+
return false if text.empty?
|
|
892
|
+
|
|
893
|
+
@editor_state.push_kill(text)
|
|
894
|
+
@editor_state.replace_selections("")
|
|
895
|
+
@editor_state.status = "Cut selection"
|
|
896
|
+
true
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def editor_search_active?
|
|
900
|
+
@editor_state&.search_active
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def editor_search_begin(direction = :forward)
|
|
904
|
+
@editor_state.begin_search(direction)
|
|
905
|
+
true
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def editor_search_append(text)
|
|
909
|
+
@editor_state.append_search(text)
|
|
910
|
+
true
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def editor_search_delete_character
|
|
914
|
+
@editor_state.delete_search_character
|
|
915
|
+
true
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def editor_search_confirm
|
|
919
|
+
@editor_state.confirm_search
|
|
920
|
+
true
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def editor_search_cancel
|
|
924
|
+
@editor_state.cancel_search
|
|
925
|
+
true
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def editor_search_repeat(direction = nil)
|
|
929
|
+
direction ||= @editor_state.search_direction
|
|
930
|
+
@editor_state.repeat_search(direction)
|
|
931
|
+
true
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def editor_search_word_under_cursor(direction = :forward)
|
|
935
|
+
query = @editor_state.word_under_cursor
|
|
936
|
+
if query.empty?
|
|
937
|
+
@editor_state.status = "No word under cursor"
|
|
938
|
+
return true
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
@editor_state.repeat_search(direction, query)
|
|
942
|
+
true
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def editor_page_rows
|
|
946
|
+
[editor_visible_line_count, 1].max
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def editor_scroll_page_rows
|
|
950
|
+
[editor_visible_line_count / 2, 1].max
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def editor_mouse_scroll_rows
|
|
954
|
+
1
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def enable_editor_mouse_reporting
|
|
958
|
+
return if @editor_mouse_reporting_enabled
|
|
959
|
+
|
|
960
|
+
@output_io.print(TerminalSequences::MOUSE_REPORTING_ENABLE)
|
|
961
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
962
|
+
@editor_mouse_reporting_enabled = true
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def disable_editor_mouse_reporting(force: false)
|
|
966
|
+
return unless force || @editor_mouse_reporting_enabled
|
|
967
|
+
|
|
968
|
+
@output_io.print(TerminalSequences::MOUSE_REPORTING_DISABLE)
|
|
969
|
+
@output_io.flush if @output_io.respond_to?(:flush)
|
|
970
|
+
@editor_mouse_reporting_enabled = false
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def scroll_editor_up(rows)
|
|
974
|
+
visible_count = editor_visible_line_count
|
|
975
|
+
@editor_state.viewport_row = [@editor_state.viewport_row - rows.to_i, 0].max
|
|
976
|
+
keep_editor_cursor_in_view(visible_count)
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def scroll_editor_down(rows)
|
|
980
|
+
visible_count = editor_visible_line_count
|
|
981
|
+
last_top_row = if current_editor_soft_wrap?
|
|
982
|
+
[editor_visual_rows(current_editor_text_width).length - visible_count, 0].max
|
|
983
|
+
else
|
|
984
|
+
[@editor_state.lines.length - visible_count, 0].max
|
|
985
|
+
end
|
|
986
|
+
@editor_state.viewport_row = [@editor_state.viewport_row + rows.to_i, last_top_row].min
|
|
987
|
+
keep_editor_cursor_in_view(visible_count)
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def keep_editor_cursor_in_view(visible_count)
|
|
991
|
+
line, column = @editor_state.cursor_line_and_column
|
|
992
|
+
if current_editor_soft_wrap?
|
|
993
|
+
text_width = current_editor_text_width
|
|
994
|
+
top_row = @editor_state.viewport_row
|
|
995
|
+
bottom_row = top_row + visible_count - 1
|
|
996
|
+
while editor_visual_row_for(*@editor_state.cursor_line_and_column, text_width) > bottom_row && @editor_state.cursor.positive?
|
|
997
|
+
editor_move_up
|
|
998
|
+
end
|
|
999
|
+
while editor_visual_row_for(*@editor_state.cursor_line_and_column, text_width) < top_row && @editor_state.cursor < @editor_state.buffer.length
|
|
1000
|
+
editor_move_down
|
|
1001
|
+
end
|
|
1002
|
+
return true
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
top_line = @editor_state.viewport_row
|
|
1006
|
+
bottom_line = top_line + visible_count - 1
|
|
1007
|
+
|
|
1008
|
+
if line < top_line
|
|
1009
|
+
@editor_state.set_cursor_line_and_column(top_line, column)
|
|
1010
|
+
elsif line > bottom_line
|
|
1011
|
+
@editor_state.set_cursor_line_and_column(bottom_line, column)
|
|
1012
|
+
end
|
|
1013
|
+
true
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
def quit_editor(message = "Unsaved changes. Press Ctrl+Q again to discard.")
|
|
1017
|
+
return false unless @editor_state
|
|
1018
|
+
return close_editor unless @editor_state.dirty?
|
|
1019
|
+
return close_editor if @editor_state.quit_confirmed
|
|
1020
|
+
|
|
1021
|
+
@editor_state.quit_confirmed = true
|
|
1022
|
+
@editor_state.status = message
|
|
1023
|
+
true
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def save_editor(path = nil, prompt_for_path: true)
|
|
1027
|
+
return false unless @editor_state
|
|
1028
|
+
if @editor_state.readonly?
|
|
1029
|
+
@editor_state.status = "Read-only diff"
|
|
1030
|
+
return true
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
if path.to_s.strip.empty? && @editor_state.path.to_s.empty?
|
|
1034
|
+
if prompt_for_path
|
|
1035
|
+
begin_editor_save_as
|
|
1036
|
+
return true
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
@editor_state.status = "Use :w filename"
|
|
1040
|
+
return false
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
return false if !path.to_s.strip.empty? && !bind_editor_save_path(path)
|
|
1044
|
+
|
|
1045
|
+
if @editor_state.file_changed_on_disk? && !@editor_state.overwrite_confirmed
|
|
1046
|
+
@editor_state.overwrite_confirmed = true
|
|
1047
|
+
@editor_state.status = "File changed on disk. Press Ctrl+S again to overwrite."
|
|
1048
|
+
return true
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
parent = File.dirname(@editor_state.path)
|
|
1052
|
+
FileUtils.mkdir_p(parent) unless Dir.exist?(parent)
|
|
1053
|
+
File.write(@editor_state.path, @editor_state.buffer)
|
|
1054
|
+
@editor_state.refresh_after_save(@editor_state.buffer)
|
|
1055
|
+
true
|
|
1056
|
+
rescue StandardError => e
|
|
1057
|
+
@editor_state.status = "Save failed: #{e.message}" if @editor_state
|
|
1058
|
+
false
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def begin_editor_save_as
|
|
1062
|
+
@editor_save_as_active = true
|
|
1063
|
+
@editor_save_as_buffer = ""
|
|
1064
|
+
@editor_state.status = "Save as: "
|
|
1065
|
+
true
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def handle_editor_save_as_key(key)
|
|
1069
|
+
return true if handle_bundled_key(key) { |token| handle_editor_save_as_key(token) }
|
|
1070
|
+
|
|
1071
|
+
csi_result = handle_editor_save_as_csi_u_key(key)
|
|
1072
|
+
return csi_result unless csi_result == false
|
|
1073
|
+
|
|
1074
|
+
case key
|
|
1075
|
+
when "\n", "\r"
|
|
1076
|
+
finish_editor_save_as
|
|
1077
|
+
when "\e", TerminalKeys::CTRL_C
|
|
1078
|
+
cancel_editor_save_as
|
|
1079
|
+
when "\b", "\x7F"
|
|
1080
|
+
@editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
|
|
1081
|
+
update_editor_save_as_status
|
|
1082
|
+
else
|
|
1083
|
+
key_name = key_name_for(key)
|
|
1084
|
+
case key_name
|
|
1085
|
+
when :return, :enter
|
|
1086
|
+
finish_editor_save_as
|
|
1087
|
+
when :backspace
|
|
1088
|
+
@editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
|
|
1089
|
+
update_editor_save_as_status
|
|
1090
|
+
else
|
|
1091
|
+
if printable_key?(key)
|
|
1092
|
+
@editor_save_as_buffer = @editor_save_as_buffer.to_s + key
|
|
1093
|
+
update_editor_save_as_status
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
true
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
def handle_editor_save_as_csi_u_key(key)
|
|
1101
|
+
sequence = parse_csi_u_key(key)
|
|
1102
|
+
return false unless sequence
|
|
1103
|
+
|
|
1104
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
1105
|
+
code = sequence[:code]
|
|
1106
|
+
modifier = sequence[:modifier]
|
|
1107
|
+
normalized_code = ctrl_code(code)
|
|
1108
|
+
if code == 13
|
|
1109
|
+
return finish_editor_save_as
|
|
1110
|
+
elsif [8, 127].include?(code)
|
|
1111
|
+
@editor_save_as_buffer = @editor_save_as_buffer.to_s[0...-1]
|
|
1112
|
+
return update_editor_save_as_status
|
|
1113
|
+
elsif code == 27 || (ctrl_modifier?(modifier) && normalized_code == 99)
|
|
1114
|
+
return cancel_editor_save_as
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
text = csi_u_printable_text(sequence)
|
|
1118
|
+
if text
|
|
1119
|
+
@editor_save_as_buffer = @editor_save_as_buffer.to_s + text
|
|
1120
|
+
return update_editor_save_as_status
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
true
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def finish_editor_save_as
|
|
1127
|
+
path = @editor_save_as_buffer.to_s.strip
|
|
1128
|
+
@editor_save_as_active = false
|
|
1129
|
+
@editor_save_as_buffer = ""
|
|
1130
|
+
if path.empty?
|
|
1131
|
+
@editor_state.status = "Save canceled"
|
|
1132
|
+
return true
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
save_editor(path, prompt_for_path: false)
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
def cancel_editor_save_as
|
|
1139
|
+
@editor_save_as_active = false
|
|
1140
|
+
@editor_save_as_buffer = ""
|
|
1141
|
+
@editor_state.status = "Save canceled"
|
|
1142
|
+
true
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def update_editor_save_as_status
|
|
1146
|
+
@editor_state.status = "Save as: #{@editor_save_as_buffer}"
|
|
1147
|
+
true
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
def bind_editor_save_path(path)
|
|
1151
|
+
full_path = File.expand_path(path.to_s.strip, Dir.pwd)
|
|
1152
|
+
root = File.expand_path(Dir.pwd)
|
|
1153
|
+
unless full_path == root || full_path.start_with?("#{root}/")
|
|
1154
|
+
@editor_state.status = "Cannot save outside workspace"
|
|
1155
|
+
return false
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
@editor_state.bind_path(full_path)
|
|
1159
|
+
@editor_syntax_language_path = nil
|
|
1160
|
+
@editor_indent_unit_path = nil
|
|
1161
|
+
true
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def run_editor_buffer
|
|
1165
|
+
return false unless @editor_state
|
|
1166
|
+
|
|
1167
|
+
language = @editor_state.language || editor_syntax_language
|
|
1168
|
+
result = ScratchpadRunner.run(language, @editor_state.buffer)
|
|
1169
|
+
@editor_state.replace_range(0, @editor_state.buffer.length, result.buffer)
|
|
1170
|
+
@editor_state.status = "Ran #{language} (exit #{result.exit_status})"
|
|
1171
|
+
true
|
|
1172
|
+
rescue StandardError => e
|
|
1173
|
+
@editor_state.status = "Run failed: #{e.message}" if @editor_state
|
|
1174
|
+
false
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
def normalize_scratchpad_language(language)
|
|
1178
|
+
case language.to_s.strip.downcase
|
|
1179
|
+
when "", "text", "txt"
|
|
1180
|
+
:text
|
|
1181
|
+
when "markdown", "md"
|
|
1182
|
+
:markdown
|
|
1183
|
+
when "ruby", "rb"
|
|
1184
|
+
:ruby
|
|
1185
|
+
else
|
|
1186
|
+
:text
|
|
1187
|
+
end
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
def scratchpad_display_path(language)
|
|
1191
|
+
case language.to_sym
|
|
1192
|
+
when :markdown
|
|
1193
|
+
"scratchpad.md"
|
|
1194
|
+
when :ruby
|
|
1195
|
+
"scratchpad.rb"
|
|
1196
|
+
else
|
|
1197
|
+
"scratchpad.txt"
|
|
1198
|
+
end
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
def scratchpad_status_text(language)
|
|
1202
|
+
runnable = language.to_sym == :ruby
|
|
1203
|
+
case current_editor_mode
|
|
1204
|
+
when "vibe"
|
|
1205
|
+
runnable ? "NORMAL · i insert · :w filename save · :q quit · :run run" : "NORMAL · i insert · :w filename save · :q quit"
|
|
1206
|
+
when "emacs"
|
|
1207
|
+
runnable ? "C-x C-s save as · C-x C-c quit · C-r run" : "C-x C-s save as · C-x C-c quit"
|
|
1208
|
+
else
|
|
1209
|
+
runnable ? "Ctrl+S save as · Ctrl+Q quit · Ctrl+R run" : "Ctrl+S save as · Ctrl+Q quit"
|
|
1210
|
+
end
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def printable_key?(key)
|
|
1214
|
+
key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
1215
|
+
end
|
|
1216
|
+
end
|
|
1217
|
+
end
|
|
1218
|
+
end
|