kward 0.83.0 → 0.84.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 +68 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +68 -21
- data/doc/editor.md +28 -13
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/platform-support.md +48 -0
- data/doc/security.md +3 -2
- data/doc/shell.md +62 -45
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +5 -5
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +6 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +118 -55
- data/lib/kward/cli/slash_commands.rb +12 -14
- data/lib/kward/cli/tabs.rb +83 -12
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +45 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/config_files.rb +82 -68
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +34 -22
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +119 -26
- data/lib/kward/project_files.rb +2 -2
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +10 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +50 -1
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +93 -18
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +2 -0
- data/lib/kward/terminal_keys.rb +12 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/registry.rb +13 -4
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +36 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -17,6 +17,9 @@ module Kward
|
|
|
17
17
|
VIBE_RUBY_BLOCK_OPENERS = %w[if unless case while until for def module class do begin].freeze
|
|
18
18
|
VIBE_RUBY_PATHS = %w[Gemfile Rakefile Guardfile Capfile Vagrantfile].freeze
|
|
19
19
|
VIBE_RUBY_EXTENSIONS = %w[.rb .rake .gemspec].freeze
|
|
20
|
+
VIBE_VISUAL_LINE_MOVE_CONTROLS = {
|
|
21
|
+
TerminalKeys::CTRL_H => "h", TerminalKeys::CTRL_J => "j", TerminalKeys::CTRL_K => "k", TerminalKeys::CTRL_L => "l"
|
|
22
|
+
}.freeze
|
|
20
23
|
|
|
21
24
|
VibeOperatorTarget = Struct.new(:type, :start_index, :end_index, :replacement_text, :replacement_cursor_offset, keyword_init: true) do
|
|
22
25
|
def characterwise?
|
|
@@ -38,6 +41,9 @@ module Kward
|
|
|
38
41
|
csi_result = handle_vibe_csi_u_key(key)
|
|
39
42
|
return csi_result unless csi_result == false
|
|
40
43
|
|
|
44
|
+
multi_cursor_result = handle_vibe_multi_cursor_key(key)
|
|
45
|
+
return multi_cursor_result unless multi_cursor_result == false
|
|
46
|
+
|
|
41
47
|
return handle_vibe_command_key(key) if @editor_state.vibe_mode == "command"
|
|
42
48
|
|
|
43
49
|
tab_result = handle_tab_key_binding(key)
|
|
@@ -84,6 +90,7 @@ module Kward
|
|
|
84
90
|
|
|
85
91
|
@editor_state.vibe_command = ""
|
|
86
92
|
@editor_state.vibe_pending = ""
|
|
93
|
+
@editor_state.vibe_command_range = nil
|
|
87
94
|
@editor_state.clear_selection
|
|
88
95
|
vibe_return_to_normal
|
|
89
96
|
end
|
|
@@ -123,6 +130,35 @@ module Kward
|
|
|
123
130
|
nil
|
|
124
131
|
end
|
|
125
132
|
|
|
133
|
+
def handle_vibe_multi_cursor_key(key)
|
|
134
|
+
return false unless key == TerminalKeys::CTRL_D
|
|
135
|
+
return false if editor_search_active?
|
|
136
|
+
|
|
137
|
+
visual_selection = @editor_state.vibe_mode == "visual"
|
|
138
|
+
return false unless visual_selection || %w[normal insert replace].include?(@editor_state.vibe_mode)
|
|
139
|
+
|
|
140
|
+
original_selection = vibe_prepare_visual_selection_for_multi_cursor if visual_selection
|
|
141
|
+
added = @editor_state.add_next_occurrence_selection
|
|
142
|
+
if added
|
|
143
|
+
vibe_enter_insert_mode(TerminalKeys::CTRL_D) unless @editor_state.vibe_mode == "insert"
|
|
144
|
+
elsif original_selection
|
|
145
|
+
@editor_state.vibe_mode = "visual"
|
|
146
|
+
@editor_state.set_selections([original_selection])
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def vibe_prepare_visual_selection_for_multi_cursor
|
|
152
|
+
range = @editor_state.selection_range
|
|
153
|
+
return unless range
|
|
154
|
+
|
|
155
|
+
original_selection = { anchor: @editor_state.selection_anchor, cursor: @editor_state.cursor }
|
|
156
|
+
vibe_remember_visual_selection
|
|
157
|
+
@editor_state.vibe_mode = "normal"
|
|
158
|
+
@editor_state.set_selections([{ anchor: range[0], cursor: range[1] }])
|
|
159
|
+
original_selection
|
|
160
|
+
end
|
|
161
|
+
|
|
126
162
|
def handle_vibe_search_key(key)
|
|
127
163
|
case key
|
|
128
164
|
when "\n", "\r"
|
|
@@ -159,7 +195,7 @@ module Kward
|
|
|
159
195
|
named_result = handle_vibe_insert_named_key(key_name) if key_name
|
|
160
196
|
return named_result unless named_result == false || named_result.nil?
|
|
161
197
|
|
|
162
|
-
vibe_record_undo {
|
|
198
|
+
vibe_record_undo { vibe_insert_printable(key) } if printable_key?(key)
|
|
163
199
|
end
|
|
164
200
|
end
|
|
165
201
|
|
|
@@ -186,9 +222,15 @@ module Kward
|
|
|
186
222
|
end
|
|
187
223
|
end
|
|
188
224
|
|
|
225
|
+
def vibe_insert_printable(text)
|
|
226
|
+
return editor_insert_printable(text) unless @editor_state.multi_cursor?
|
|
227
|
+
|
|
228
|
+
@editor_state.replace_selections(text)
|
|
229
|
+
end
|
|
230
|
+
|
|
189
231
|
def vibe_replace_character(key)
|
|
190
232
|
@editor_state.delete_at_cursor
|
|
191
|
-
|
|
233
|
+
vibe_insert_printable(key)
|
|
192
234
|
end
|
|
193
235
|
|
|
194
236
|
def handle_vibe_command_key(key)
|
|
@@ -196,6 +238,7 @@ module Kward
|
|
|
196
238
|
case key
|
|
197
239
|
when "\e", TerminalKeys::CTRL_C, :escape
|
|
198
240
|
@editor_state.vibe_command = ""
|
|
241
|
+
@editor_state.vibe_command_range = nil
|
|
199
242
|
vibe_return_to_normal
|
|
200
243
|
when "\b", "\x7F"
|
|
201
244
|
@editor_state.vibe_command = @editor_state.vibe_command[0...-1].to_s
|
|
@@ -215,6 +258,10 @@ module Kward
|
|
|
215
258
|
|
|
216
259
|
def execute_vibe_command(command)
|
|
217
260
|
command = command.to_s.strip
|
|
261
|
+
command_range = @editor_state.vibe_command_range
|
|
262
|
+
command_range ||= vibe_visual_line_bounds if vibe_visual_mode?
|
|
263
|
+
command_range, command = vibe_extract_command_range(command, command_range)
|
|
264
|
+
@editor_state.vibe_command_range = nil
|
|
218
265
|
@editor_state.vibe_mode = "normal"
|
|
219
266
|
@editor_state.vibe_command = ""
|
|
220
267
|
action = nil
|
|
@@ -225,8 +272,15 @@ module Kward
|
|
|
225
272
|
save_editor(Regexp.last_match(1))
|
|
226
273
|
when /\Ae(!?)\s+(.+)\z/
|
|
227
274
|
vibe_edit_file(Regexp.last_match(2), force: Regexp.last_match(1) == "!")
|
|
275
|
+
when "run all"
|
|
276
|
+
run_vibe_all_markdown_code_blocks
|
|
228
277
|
when "run"
|
|
229
|
-
|
|
278
|
+
block = vibe_markdown_code_block_for_run(command_range)
|
|
279
|
+
if block
|
|
280
|
+
run_vibe_markdown_code_block(block)
|
|
281
|
+
else
|
|
282
|
+
vibe_record_undo { run_editor_buffer(source: vibe_command_source(command_range)) }
|
|
283
|
+
end
|
|
230
284
|
when /\Aprompt\s+(.+)\z/
|
|
231
285
|
action = editor_prompt_action(Regexp.last_match(1))
|
|
232
286
|
when "prompt"
|
|
@@ -243,7 +297,7 @@ module Kward
|
|
|
243
297
|
save_editor if @editor_state&.dirty?
|
|
244
298
|
close_editor if @editor_state
|
|
245
299
|
when /\A(?:(%|\d+,\d+))?s\/([^\/]*)\/([^\/]*)\/(g?)\z/
|
|
246
|
-
vibe_substitute_command(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3), global: Regexp.last_match(4) == "g")
|
|
300
|
+
vibe_substitute_command(command_range || Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3), global: Regexp.last_match(4) == "g")
|
|
247
301
|
when /\A\d+\z/
|
|
248
302
|
@editor_state.set_cursor_line_and_column(command.to_i - 1, 0)
|
|
249
303
|
@editor_state.status = "Line #{command}"
|
|
@@ -253,6 +307,134 @@ module Kward
|
|
|
253
307
|
action || true
|
|
254
308
|
end
|
|
255
309
|
|
|
310
|
+
def vibe_extract_command_range(command, visual_range)
|
|
311
|
+
if command.start_with?("'<,'>")
|
|
312
|
+
[visual_range, command[5..].to_s.strip]
|
|
313
|
+
elsif (match = command.match(/\A(%|\d+,\d+)(?=[a-z])/))
|
|
314
|
+
[match[1], command[match[1].length..].to_s]
|
|
315
|
+
else
|
|
316
|
+
[visual_range, command]
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def vibe_command_source(range)
|
|
321
|
+
return @editor_state.buffer.dup unless range
|
|
322
|
+
|
|
323
|
+
first_line, last_line = vibe_command_line_bounds(range)
|
|
324
|
+
start_index = @editor_state.line_range(first_line)[0]
|
|
325
|
+
end_index = @editor_state.line_range(last_line)[1]
|
|
326
|
+
@editor_state.buffer[start_index...end_index].to_s
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def vibe_command_line_bounds(range)
|
|
330
|
+
return range if range.is_a?(Array)
|
|
331
|
+
return [0, @editor_state.lines.length - 1] if range.to_s == "%"
|
|
332
|
+
|
|
333
|
+
first_line, last_line = range.to_s.split(",", 2).map { |value| value.to_i - 1 }
|
|
334
|
+
[first_line, last_line]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def vibe_markdown_code_block_for_run(command_range)
|
|
338
|
+
if command_range.is_a?(Array)
|
|
339
|
+
MarkdownCodeBlock.for_selection(@editor_state.buffer, *command_range)
|
|
340
|
+
else
|
|
341
|
+
line, = @editor_state.cursor_line_and_column
|
|
342
|
+
MarkdownCodeBlock.for_cursor(@editor_state.buffer, line)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def run_vibe_markdown_code_block(block)
|
|
347
|
+
document = block.source.dup
|
|
348
|
+
language = normalize_vibe_code_block_language(block.language)
|
|
349
|
+
unless language
|
|
350
|
+
@editor_state.status = "Selected code block has no runnable language"
|
|
351
|
+
return false
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
start_vibe_markdown_code_block_run(block, language) do |result, error|
|
|
355
|
+
if @editor_state.buffer != document
|
|
356
|
+
@editor_state.status = "Output not inserted: buffer changed"
|
|
357
|
+
next
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
output = error ? "Run failed: #{error.message}" : result.output
|
|
361
|
+
@editor_state.replace_buffer(block.with_output(output))
|
|
362
|
+
@editor_state.status = error ? "Output updated · Run failed" : vibe_code_block_result_status(result)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def run_vibe_all_markdown_code_blocks
|
|
367
|
+
document = @editor_state.buffer.dup
|
|
368
|
+
runs = MarkdownCodeBlock.parse(document).filter_map do |block|
|
|
369
|
+
language = normalize_vibe_code_block_language(block.language)
|
|
370
|
+
[block, language] if language
|
|
371
|
+
end
|
|
372
|
+
if runs.empty?
|
|
373
|
+
@editor_state.status = "No runnable code blocks"
|
|
374
|
+
return false
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
results = []
|
|
378
|
+
run_next = nil
|
|
379
|
+
run_next = lambda do
|
|
380
|
+
block, language = runs.shift
|
|
381
|
+
start_vibe_markdown_code_block_run(block, language) do |result, error|
|
|
382
|
+
output = error ? "Run failed: #{error.message}" : result.output
|
|
383
|
+
results << [block, output, result, error]
|
|
384
|
+
if runs.empty?
|
|
385
|
+
finish_vibe_all_markdown_code_blocks(document, results)
|
|
386
|
+
else
|
|
387
|
+
run_next.call
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
run_next.call
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def start_vibe_markdown_code_block_run(block, language, &completion)
|
|
395
|
+
run_editor_buffer(
|
|
396
|
+
source: block.code,
|
|
397
|
+
language: language,
|
|
398
|
+
source_path: ScratchpadLanguages.display_path(language),
|
|
399
|
+
output_visible: false,
|
|
400
|
+
completion: completion
|
|
401
|
+
)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def finish_vibe_all_markdown_code_blocks(document, results)
|
|
405
|
+
if @editor_state.buffer != document
|
|
406
|
+
@editor_state.status = "Output not inserted: buffer changed"
|
|
407
|
+
return false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
content = document.dup
|
|
411
|
+
results.reverse_each do |block, output, _result, _error|
|
|
412
|
+
start_index, end_index, replacement = block.output_edit(output)
|
|
413
|
+
content[start_index...end_index] = replacement
|
|
414
|
+
end
|
|
415
|
+
@editor_state.replace_buffer(content)
|
|
416
|
+
failures = results.count { |_block, _output, result, error| error || result.exit_status != 0 }
|
|
417
|
+
status = "Ran #{results.length} code blocks"
|
|
418
|
+
status += " · #{failures} failed" if failures.positive?
|
|
419
|
+
@editor_state.status = status
|
|
420
|
+
true
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def vibe_code_block_result_status(result)
|
|
424
|
+
return "Output updated · Canceled" if result.cancelled
|
|
425
|
+
|
|
426
|
+
"Output updated · Exit #{result.exit_status} · #{format_runner_duration(result.duration)}"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def normalize_vibe_code_block_language(language)
|
|
430
|
+
return nil if language.to_s.strip.empty?
|
|
431
|
+
|
|
432
|
+
normalized = ScratchpadLanguages.normalize(language)
|
|
433
|
+
ScratchpadLanguages.runnable?(normalized) ? normalized : nil
|
|
434
|
+
rescue ArgumentError
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
|
|
256
438
|
def vibe_edit_file(path, force: false)
|
|
257
439
|
if @editor_state.dirty? && !force
|
|
258
440
|
@editor_state.status = "No write since last change (:e! overrides)"
|
|
@@ -264,6 +446,8 @@ module Kward
|
|
|
264
446
|
|
|
265
447
|
def vibe_complete_command_path
|
|
266
448
|
command = @editor_state.vibe_command.to_s
|
|
449
|
+
range_prefix = command.start_with?("'<,'>") ? command[0, 5] : ""
|
|
450
|
+
command = command[5..].to_s if range_prefix != ""
|
|
267
451
|
match = command.match(/\A(e!?)\s+(.*)\z/)
|
|
268
452
|
return false unless match
|
|
269
453
|
|
|
@@ -276,7 +460,7 @@ module Kward
|
|
|
276
460
|
|
|
277
461
|
replacement = candidates.length == 1 ? candidates.first : vibe_common_prefix(candidates)
|
|
278
462
|
if replacement.length > prefix.length
|
|
279
|
-
@editor_state.vibe_command = "#{match[1]} #{replacement}"
|
|
463
|
+
@editor_state.vibe_command = "#{range_prefix}#{match[1]} #{replacement}"
|
|
280
464
|
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
281
465
|
elsif candidates.length > 1
|
|
282
466
|
@editor_state.status = vibe_path_completion_status(candidates)
|
|
@@ -336,7 +520,9 @@ module Kward
|
|
|
336
520
|
|
|
337
521
|
start_line = 0
|
|
338
522
|
end_line = @editor_state.lines.length - 1
|
|
339
|
-
if range
|
|
523
|
+
if range.is_a?(Array)
|
|
524
|
+
start_line, end_line = range
|
|
525
|
+
elsif range&.include?(",")
|
|
340
526
|
start_line, end_line = range.split(",", 2).map { |value| value.to_i - 1 }
|
|
341
527
|
end
|
|
342
528
|
start_line = [[start_line, 0].max, @editor_state.lines.length - 1].min
|
|
@@ -406,8 +592,6 @@ module Kward
|
|
|
406
592
|
@editor_state.page_up(editor_page_rows)
|
|
407
593
|
when :ctrl_f
|
|
408
594
|
@editor_state.page_down(editor_page_rows)
|
|
409
|
-
when :ctrl_d
|
|
410
|
-
@editor_state.page_down(vibe_half_page_rows)
|
|
411
595
|
when :ctrl_u
|
|
412
596
|
@editor_state.page_up(vibe_half_page_rows)
|
|
413
597
|
when :ctrl_e
|
|
@@ -422,7 +606,7 @@ module Kward
|
|
|
422
606
|
end
|
|
423
607
|
|
|
424
608
|
def vibe_normal_control_key?(key)
|
|
425
|
-
["\n", "\r", "\b", "\x7F", TerminalKeys::CTRL_B, TerminalKeys::
|
|
609
|
+
["\n", "\r", "\b", "\x7F", TerminalKeys::CTRL_B, TerminalKeys::CTRL_E, TerminalKeys::CTRL_F, "\x0F", TerminalKeys::CTRL_R, TerminalKeys::CTRL_U, TerminalKeys::CTRL_Y].include?(key)
|
|
426
610
|
end
|
|
427
611
|
|
|
428
612
|
def vibe_visual_mode?
|
|
@@ -430,7 +614,7 @@ module Kward
|
|
|
430
614
|
end
|
|
431
615
|
|
|
432
616
|
def vibe_return_to_normal
|
|
433
|
-
|
|
617
|
+
@editor_state.collapse_to_primary_selection if @editor_state.multi_cursor?
|
|
434
618
|
@editor_state.vibe_mode = "normal"
|
|
435
619
|
@editor_state.status = "NORMAL · i insert · :prompt ask agent · :w save · :q quit"
|
|
436
620
|
true
|
|
@@ -551,8 +735,6 @@ module Kward
|
|
|
551
735
|
@editor_state.page_down(editor_page_rows)
|
|
552
736
|
when TerminalKeys::CTRL_B
|
|
553
737
|
@editor_state.page_up(editor_page_rows)
|
|
554
|
-
when TerminalKeys::CTRL_D
|
|
555
|
-
@editor_state.page_down(vibe_half_page_rows)
|
|
556
738
|
when TerminalKeys::CTRL_U
|
|
557
739
|
@editor_state.page_up(vibe_half_page_rows)
|
|
558
740
|
when TerminalKeys::CTRL_E
|
|
@@ -653,6 +835,7 @@ module Kward
|
|
|
653
835
|
when "u"
|
|
654
836
|
@editor_state.undo
|
|
655
837
|
when ":"
|
|
838
|
+
@editor_state.vibe_command_range = nil
|
|
656
839
|
@editor_state.vibe_mode = "command"
|
|
657
840
|
@editor_state.vibe_command = ""
|
|
658
841
|
@editor_state.status = ":"
|
|
@@ -674,6 +857,13 @@ module Kward
|
|
|
674
857
|
end
|
|
675
858
|
|
|
676
859
|
def handle_vibe_visual_key(key)
|
|
860
|
+
if (direction = VIBE_VISUAL_LINE_MOVE_CONTROLS[key])
|
|
861
|
+
count, = vibe_count_and_body(@editor_state.vibe_pending.to_s + direction)
|
|
862
|
+
count = 1 if count.zero?
|
|
863
|
+
@editor_state.vibe_pending = ""
|
|
864
|
+
return vibe_move_selected_lines(direction, count)
|
|
865
|
+
end
|
|
866
|
+
|
|
677
867
|
key_name = key_name_for(key)
|
|
678
868
|
return handle_vibe_visual_named_key(key_name) if key_name
|
|
679
869
|
if key == "\e" || key == TerminalKeys::CTRL_C
|
|
@@ -708,9 +898,9 @@ module Kward
|
|
|
708
898
|
when "p"
|
|
709
899
|
vibe_paste_visual_selection
|
|
710
900
|
when "I"
|
|
711
|
-
|
|
901
|
+
vibe_begin_visual_multi_cursor_insert(:before)
|
|
712
902
|
when "A"
|
|
713
|
-
|
|
903
|
+
vibe_begin_visual_multi_cursor_insert(:after)
|
|
714
904
|
when ">"
|
|
715
905
|
vibe_indent_visual_selection(:right)
|
|
716
906
|
when "<"
|
|
@@ -735,6 +925,8 @@ module Kward
|
|
|
735
925
|
editor_search_repeat(vibe_opposite_search_direction)
|
|
736
926
|
when "o"
|
|
737
927
|
vibe_switch_visual_selection_end
|
|
928
|
+
when ":"
|
|
929
|
+
vibe_begin_visual_command
|
|
738
930
|
when "G"
|
|
739
931
|
vibe_visual_goto_line(command.match?(/\A\d+G\z/) ? count : nil)
|
|
740
932
|
when "gg"
|
|
@@ -785,6 +977,14 @@ module Kward
|
|
|
785
977
|
true
|
|
786
978
|
end
|
|
787
979
|
|
|
980
|
+
def vibe_begin_visual_command
|
|
981
|
+
@editor_state.vibe_command_range = vibe_visual_line_bounds
|
|
982
|
+
@editor_state.vibe_mode = "command"
|
|
983
|
+
@editor_state.vibe_command = "'<,'>"
|
|
984
|
+
@editor_state.status = ":'<,'>"
|
|
985
|
+
true
|
|
986
|
+
end
|
|
987
|
+
|
|
788
988
|
def vibe_jump_ruby_method(direction)
|
|
789
989
|
unless vibe_ruby_file?
|
|
790
990
|
@editor_state.status = "Ruby navigation requires Ruby file"
|
|
@@ -958,6 +1158,71 @@ module Kward
|
|
|
958
1158
|
vibe_apply_cursor_motion(motion, count)
|
|
959
1159
|
end
|
|
960
1160
|
|
|
1161
|
+
def vibe_move_selected_lines(direction, count)
|
|
1162
|
+
@editor_state.status = case @editor_state.vibe_mode
|
|
1163
|
+
when "visual_line" then "VISUAL LINE"
|
|
1164
|
+
when "visual_block" then "VISUAL BLOCK"
|
|
1165
|
+
else "VISUAL"
|
|
1166
|
+
end
|
|
1167
|
+
start_line, end_line = vibe_visual_line_bounds
|
|
1168
|
+
original_lines = @editor_state.lines.dup
|
|
1169
|
+
lines = original_lines.dup
|
|
1170
|
+
endpoint_positions = [@editor_state.selection_anchor, @editor_state.cursor].map do |offset|
|
|
1171
|
+
@editor_state.cursor_line_and_column_for(offset)
|
|
1172
|
+
end
|
|
1173
|
+
line_shift = 0
|
|
1174
|
+
|
|
1175
|
+
case direction
|
|
1176
|
+
when "h", "l"
|
|
1177
|
+
moved_lines = lines[start_line..end_line].map(&:dup)
|
|
1178
|
+
count.times do
|
|
1179
|
+
moved_lines = moved_lines.map do |line|
|
|
1180
|
+
if direction == "l"
|
|
1181
|
+
" #{line}"
|
|
1182
|
+
else
|
|
1183
|
+
line.sub(/\A(?: |\t| )/, "")
|
|
1184
|
+
end
|
|
1185
|
+
end
|
|
1186
|
+
end
|
|
1187
|
+
lines[start_line..end_line] = moved_lines
|
|
1188
|
+
when "k"
|
|
1189
|
+
line_shift = -[count, start_line].min
|
|
1190
|
+
return true if line_shift.zero?
|
|
1191
|
+
|
|
1192
|
+
selected_lines = lines.slice!(start_line..end_line)
|
|
1193
|
+
lines.insert(start_line + line_shift, *selected_lines)
|
|
1194
|
+
when "j"
|
|
1195
|
+
line_shift = [count, lines.length - end_line - 1].min
|
|
1196
|
+
return true if line_shift.zero?
|
|
1197
|
+
|
|
1198
|
+
selected_lines = lines.slice!(start_line..end_line)
|
|
1199
|
+
lines.insert(start_line + line_shift, *selected_lines)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
return false if lines == @editor_state.lines
|
|
1203
|
+
|
|
1204
|
+
@editor_state.replace_buffer(lines.join("\n"))
|
|
1205
|
+
selection_offsets = endpoint_positions.map do |line, column|
|
|
1206
|
+
new_line = line + line_shift
|
|
1207
|
+
new_column = if direction == "l"
|
|
1208
|
+
column + (lines[line].length - original_lines[line].length)
|
|
1209
|
+
elsif direction == "h"
|
|
1210
|
+
[column - (original_lines[line].length - lines[line].length), 0].max
|
|
1211
|
+
else
|
|
1212
|
+
column
|
|
1213
|
+
end
|
|
1214
|
+
@editor_state.offset_for_line_and_column(new_line, new_column)
|
|
1215
|
+
end
|
|
1216
|
+
@editor_state.set_selections([{ anchor: selection_offsets[0], cursor: selection_offsets[1] }])
|
|
1217
|
+
true
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def vibe_visual_line_bounds
|
|
1221
|
+
anchor_line, = @editor_state.cursor_line_and_column_for(@editor_state.selection_anchor)
|
|
1222
|
+
cursor_line, = @editor_state.cursor_line_and_column
|
|
1223
|
+
[anchor_line, cursor_line].minmax
|
|
1224
|
+
end
|
|
1225
|
+
|
|
961
1226
|
def vibe_visual_range
|
|
962
1227
|
@editor_state.selection_range
|
|
963
1228
|
end
|
|
@@ -1013,39 +1278,19 @@ module Kward
|
|
|
1013
1278
|
vibe_cancel_visual_mode
|
|
1014
1279
|
end
|
|
1015
1280
|
|
|
1016
|
-
def
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
@editor_state.
|
|
1025
|
-
|
|
1026
|
-
@editor_state.set_cursor_line_and_column(start_line, column)
|
|
1027
|
-
@editor_state.vibe_visual_block_insert[:start_index] = @editor_state.cursor
|
|
1028
|
-
@editor_state.vibe_mode = "insert"
|
|
1029
|
-
@editor_state.status = "INSERT · Esc normal"
|
|
1281
|
+
def vibe_begin_visual_multi_cursor_insert(position)
|
|
1282
|
+
vibe_remember_visual_selection
|
|
1283
|
+
start_line, end_line = vibe_visual_line_bounds
|
|
1284
|
+
selections = (start_line..end_line).map do |line_index|
|
|
1285
|
+
column = position == :before ? 0 : @editor_state.lines[line_index].to_s.length
|
|
1286
|
+
offset = @editor_state.offset_for_line_and_column(line_index, column)
|
|
1287
|
+
{ anchor: offset, cursor: offset }
|
|
1288
|
+
end
|
|
1289
|
+
@editor_state.set_selections(selections)
|
|
1290
|
+
vibe_enter_insert_mode(position == :before ? "I" : "A")
|
|
1030
1291
|
true
|
|
1031
1292
|
end
|
|
1032
1293
|
|
|
1033
|
-
def vibe_apply_visual_block_insert
|
|
1034
|
-
block = @editor_state.vibe_visual_block_insert
|
|
1035
|
-
@editor_state.vibe_visual_block_insert = nil
|
|
1036
|
-
return unless block
|
|
1037
|
-
|
|
1038
|
-
inserted_text = @editor_state.buffer[block[:start_index]...@editor_state.cursor].to_s
|
|
1039
|
-
return if inserted_text.empty?
|
|
1040
|
-
|
|
1041
|
-
block[:end_line].downto(block[:start_line] + 1) do |line_index|
|
|
1042
|
-
line_start = @editor_state.line_start_offset(line_index)
|
|
1043
|
-
line_length = @editor_state.lines[line_index].to_s.length
|
|
1044
|
-
@editor_state.cursor = line_start + [block[:column], line_length].min
|
|
1045
|
-
@editor_state.insert(inserted_text)
|
|
1046
|
-
end
|
|
1047
|
-
end
|
|
1048
|
-
|
|
1049
1294
|
def vibe_transform_visual_selection(transform)
|
|
1050
1295
|
range = vibe_visual_range
|
|
1051
1296
|
return false unless range
|
|
@@ -7,6 +7,8 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def editor_layout(width, height = screen_height)
|
|
10
|
+
return editor_split_layout(width, height) if editor_runner_output_visible?
|
|
11
|
+
|
|
10
12
|
content_width = [width - 4, 1].max
|
|
11
13
|
visible_count = editor_visible_line_count(height: height, width: width)
|
|
12
14
|
line_index, column = @editor_state.cursor_line_and_column
|
|
@@ -21,7 +23,107 @@ module Kward
|
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
26
|
+
def editor_split_layout(width, height)
|
|
27
|
+
content_width = [width - 4, 1].max
|
|
28
|
+
source_count, output_count = editor_split_visible_counts(height: height, width: width)
|
|
29
|
+
line_index, column = @editor_state.cursor_line_and_column
|
|
30
|
+
gutter_width = editor_line_number_gutter_width
|
|
31
|
+
text_width = editor_text_width(content_width, gutter_width)
|
|
32
|
+
sync_editor_wrap_state(text_width)
|
|
33
|
+
source_rows, cursor_row, cursor_col = if current_editor_soft_wrap?
|
|
34
|
+
editor_wrapped_layout(width, content_width, source_count, line_index, column, text_width, include_bottom: false)
|
|
35
|
+
else
|
|
36
|
+
editor_unwrapped_layout(width, content_width, source_count, line_index, column, text_width, include_bottom: false)
|
|
37
|
+
end
|
|
38
|
+
rows = source_rows + editor_runner_output_rows(width, content_width, output_count)
|
|
39
|
+
rows.concat(editor_bottom_rows(width))
|
|
40
|
+
[rows, cursor_row, cursor_col]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def editor_split_visible_counts(height:, width:)
|
|
44
|
+
# Keep one row available above the composer, matching the normal editor layout.
|
|
45
|
+
available = height - editor_bottom_rows(width).length - 5
|
|
46
|
+
available = 2 if available < 2
|
|
47
|
+
source_count = (available + 1) / 2
|
|
48
|
+
[source_count, available - source_count]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def editor_runner_output_rows(width, content_width, visible_count, include_bottom: false)
|
|
52
|
+
lines = editor_runner_output_lines(content_width)
|
|
53
|
+
maximum_scroll = [lines.length - visible_count, 0].max
|
|
54
|
+
@editor_runner_state.set_scroll_row(@editor_runner_state.scroll_row, maximum: maximum_scroll)
|
|
55
|
+
visible_lines = lines[@editor_runner_state.scroll_row, visible_count] || []
|
|
56
|
+
visible_lines << "" while visible_lines.length < visible_count
|
|
57
|
+
rows = [editor_runner_output_top_border(width)]
|
|
58
|
+
rows.concat(visible_lines.each_with_index.map do |line, index|
|
|
59
|
+
line_index = @editor_runner_state.scroll_row + index
|
|
60
|
+
selection = editor_runner_selection_range_for(line_index, ANSI.strip(line).length)
|
|
61
|
+
line = editor_overlay_selection(line, [selection], force: true) if selection
|
|
62
|
+
box_content_row(line, content_width)
|
|
63
|
+
end)
|
|
64
|
+
rows << footer_row(content_width, editor_runner_output_status)
|
|
65
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
66
|
+
rows
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def editor_runner_output_visible_count(height: screen_height, width: screen_width)
|
|
70
|
+
editor_split_visible_counts(height: height, width: width).last
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def editor_runner_output_scroll_max
|
|
74
|
+
width = [screen_width - 4, 1].max
|
|
75
|
+
lines = editor_runner_output_lines(width)
|
|
76
|
+
[lines.length - editor_runner_output_visible_count, 0].max
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def editor_runner_output_lines(width)
|
|
80
|
+
text = ANSI.sanitize_transcript(@editor_runner_state.output_text)
|
|
81
|
+
text = ANSI.strip(text) unless @color_enabled
|
|
82
|
+
text = text.chomp
|
|
83
|
+
return ["(no output)"] if text.empty?
|
|
84
|
+
|
|
85
|
+
text.split("\n", -1).flat_map do |line|
|
|
86
|
+
wrapped = ANSI.wrap_visible(line, width)
|
|
87
|
+
wrapped.empty? ? [""] : wrapped
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def editor_runner_output_top_border(width)
|
|
92
|
+
title = visible_truncate("Output #{editor_display_path}", [width - 4, 1].max)
|
|
93
|
+
plain_title = ANSI.strip(title)
|
|
94
|
+
"#{colored("╭", :primary_green)} #{title} #{colored("─" * [width - plain_title.length - 4, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def editor_runner_output_status
|
|
98
|
+
state = @editor_runner_state
|
|
99
|
+
if state.running?
|
|
100
|
+
return "Canceling #{state.language} · please wait" if state.cancel_requested?
|
|
101
|
+
|
|
102
|
+
return "Running #{state.language} · Ctrl+C cancel · Esc close"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if (error = state.error)
|
|
106
|
+
return "Run failed: #{error.message} · Esc close"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
result = state.result
|
|
110
|
+
status = if result.cancelled
|
|
111
|
+
"Canceled"
|
|
112
|
+
else
|
|
113
|
+
"Exit #{result.exit_status}"
|
|
114
|
+
end
|
|
115
|
+
status += " · #{format_runner_duration(result.duration)}"
|
|
116
|
+
status += " · output truncated" if result.truncated
|
|
117
|
+
status += " · source changed" if editor_runner_output_stale?
|
|
118
|
+
"#{status} · Ctrl+R rerun · Esc close"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def editor_runner_output_stale?
|
|
122
|
+
digest = Digest::SHA256.hexdigest(@editor_state.buffer.to_s)
|
|
123
|
+
digest != @editor_runner_state.source_digest
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def editor_unwrapped_layout(width, content_width, visible_count, line_index, column, text_width, include_bottom: true)
|
|
25
127
|
@editor_state.viewport_row = [[@editor_state.viewport_row, line_index - visible_count + 1].max, line_index].min
|
|
26
128
|
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
27
129
|
@editor_state.viewport_column = [[@editor_state.viewport_column.to_i, column - text_width + 1].max, column].min
|
|
@@ -43,13 +145,13 @@ module Kward
|
|
|
43
145
|
box_content_row(row, content_width)
|
|
44
146
|
end)
|
|
45
147
|
rows << footer_row(content_width, editor_status_text)
|
|
46
|
-
rows.concat(editor_bottom_rows(width))
|
|
148
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
47
149
|
cursor_row = 1 + line_index - @editor_state.viewport_row
|
|
48
150
|
cursor_col = 2 + gutter_width + [[column - @editor_state.viewport_column, 0].max, text_width - 1].min
|
|
49
151
|
[rows, cursor_row, cursor_col]
|
|
50
152
|
end
|
|
51
153
|
|
|
52
|
-
def editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
154
|
+
def editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width, include_bottom: true)
|
|
53
155
|
visual_rows = editor_visual_rows(text_width)
|
|
54
156
|
cursor_visual_row = editor_visual_row_for(line_index, column, text_width)
|
|
55
157
|
@editor_state.viewport_row = [[@editor_state.viewport_row, cursor_visual_row - visible_count + 1].max, cursor_visual_row].min
|
|
@@ -67,7 +169,7 @@ module Kward
|
|
|
67
169
|
end
|
|
68
170
|
end)
|
|
69
171
|
rows << footer_row(content_width, editor_status_text)
|
|
70
|
-
rows.concat(editor_bottom_rows(width))
|
|
172
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
71
173
|
line_start = editor_visual_row_start_column(line_index, column, text_width)
|
|
72
174
|
cursor_row = 1 + cursor_visual_row - @editor_state.viewport_row
|
|
73
175
|
cursor_col = 2 + editor_line_number_gutter_width + [[column - line_start, 0].max, text_width - 1].min
|
|
@@ -138,8 +240,8 @@ module Kward
|
|
|
138
240
|
editor_overlay_selection(rendered, cursor_columns.map { |column| [column, column + 1] })
|
|
139
241
|
end
|
|
140
242
|
|
|
141
|
-
def editor_overlay_selection(rendered, selection_ranges)
|
|
142
|
-
return rendered unless @color_enabled
|
|
243
|
+
def editor_overlay_selection(rendered, selection_ranges, force: false)
|
|
244
|
+
return rendered unless @color_enabled || force
|
|
143
245
|
|
|
144
246
|
output = +""
|
|
145
247
|
selected = false
|