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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -0
  5. data/doc/agent-tools.md +15 -6
  6. data/doc/authentication.md +22 -1
  7. data/doc/code-search.md +42 -2
  8. data/doc/configuration.md +106 -3
  9. data/doc/context-budgeting.md +136 -0
  10. data/doc/context-tools.md +16 -3
  11. data/doc/editor.md +394 -0
  12. data/doc/extensibility.md +16 -7
  13. data/doc/files.md +100 -0
  14. data/doc/getting-started.md +25 -18
  15. data/doc/git.md +122 -0
  16. data/doc/memory.md +24 -4
  17. data/doc/personas.md +34 -5
  18. data/doc/plugins.md +72 -1
  19. data/doc/releasing.md +37 -9
  20. data/doc/rpc.md +74 -4
  21. data/doc/session-management.md +35 -1
  22. data/doc/shell.md +286 -0
  23. data/doc/tabs.md +122 -0
  24. data/doc/troubleshooting.md +77 -1
  25. data/doc/usage.md +53 -7
  26. data/doc/web-search.md +12 -4
  27. data/doc/workspace-tools.md +51 -12
  28. data/examples/plugins/space_invaders.rb +377 -0
  29. data/lib/kward/agent.rb +1 -1
  30. data/lib/kward/cli/commands.rb +33 -2
  31. data/lib/kward/cli/git.rb +150 -0
  32. data/lib/kward/cli/interactive_turn.rb +73 -9
  33. data/lib/kward/cli/plugins.rb +54 -4
  34. data/lib/kward/cli/prompt_interface.rb +32 -1
  35. data/lib/kward/cli/runtime_helpers.rb +133 -3
  36. data/lib/kward/cli/sessions.rb +2 -2
  37. data/lib/kward/cli/settings.rb +218 -9
  38. data/lib/kward/cli/slash_commands.rb +415 -2
  39. data/lib/kward/cli/tabs.rb +695 -0
  40. data/lib/kward/cli.rb +158 -26
  41. data/lib/kward/config_files.rb +123 -1
  42. data/lib/kward/context_budget_meter.rb +44 -0
  43. data/lib/kward/conversation.rb +12 -4
  44. data/lib/kward/editor_mode.rb +25 -0
  45. data/lib/kward/ekwsh.rb +362 -0
  46. data/lib/kward/plugin_registry.rb +61 -0
  47. data/lib/kward/project_files.rb +52 -0
  48. data/lib/kward/prompt_history.rb +82 -0
  49. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  50. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  51. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  52. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  53. data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
  54. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  55. data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
  56. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  57. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  58. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  59. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  60. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  61. data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
  62. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
  63. data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
  64. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  65. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  66. data/lib/kward/prompt_interface/editor/state.rb +1249 -0
  67. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  68. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
  69. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  70. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  71. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  72. data/lib/kward/prompt_interface/git_prompt.rb +299 -0
  73. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  74. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  75. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  76. data/lib/kward/prompt_interface/key_handler.rb +387 -35
  77. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  78. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  79. data/lib/kward/prompt_interface/question_prompt.rb +98 -50
  80. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  81. data/lib/kward/prompt_interface/screen.rb +16 -0
  82. data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
  83. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  84. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  85. data/lib/kward/prompt_interface.rb +286 -8
  86. data/lib/kward/prompts/commands.rb +5 -0
  87. data/lib/kward/prompts.rb +2 -0
  88. data/lib/kward/rpc/server.rb +42 -3
  89. data/lib/kward/rpc/session_manager.rb +35 -47
  90. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  91. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  92. data/lib/kward/session_store.rb +44 -0
  93. data/lib/kward/session_tree_nodes.rb +136 -0
  94. data/lib/kward/session_tree_renderer.rb +9 -131
  95. data/lib/kward/tab_store.rb +47 -0
  96. data/lib/kward/text_boundary.rb +25 -0
  97. data/lib/kward/tools/context_budget_stats.rb +54 -0
  98. data/lib/kward/tools/context_for_task.rb +202 -0
  99. data/lib/kward/tools/read_file.rb +8 -4
  100. data/lib/kward/tools/registry.rb +62 -16
  101. data/lib/kward/tools/tool_call.rb +10 -0
  102. data/lib/kward/version.rb +1 -1
  103. data/lib/kward/workers/git_guard.rb +68 -0
  104. data/lib/kward/workers/live_view.rb +49 -0
  105. data/lib/kward/workers/manager.rb +288 -0
  106. data/lib/kward/workers/store.rb +72 -0
  107. data/lib/kward/workers/tool_policy.rb +23 -0
  108. data/lib/kward/workers/worker.rb +82 -0
  109. data/lib/kward/workers/write_lock.rb +38 -0
  110. data/lib/kward/workers.rb +7 -0
  111. data/lib/kward/workspace.rb +110 -24
  112. data/templates/default/fulldoc/html/css/kward.css +107 -36
  113. data/templates/default/kward_navigation.rb +12 -1
  114. data/templates/default/layout/html/layout.erb +4 -2
  115. data/templates/default/layout/html/setup.rb +6 -0
  116. metadata +53 -1
@@ -0,0 +1,1249 @@
1
+ require_relative "../../editor_mode"
2
+ require_relative "../../text_boundary"
3
+ require_relative "buffer"
4
+ require_relative "file_marker"
5
+ require_relative "indent_navigation"
6
+ require_relative "kill_ring"
7
+ require_relative "undo_history"
8
+ require_relative "search"
9
+ require_relative "selections"
10
+ require_relative "status_text"
11
+ require_relative "vibe_state"
12
+
13
+ # Namespace for the Kward CLI agent runtime.
14
+ module Kward
15
+ # Interactive terminal UI used by the CLI frontend.
16
+ class PromptInterface
17
+ # Mutable state for the built-in composer file editor.
18
+ class EditorState
19
+ attr_reader :path, :original_content, :original_digest, :original_mtime, :original_size
20
+ attr_reader :buffer, :undo_stack, :redo_stack, :kill_buffer, :kill_ring, :last_yank_range, :last_yank_index
21
+ attr_accessor :viewport_row, :viewport_column, :status, :overwrite_confirmed, :quit_confirmed, :search_active, :search_query, :search_direction, :new_file, :editor_mode, :emacs_pending, :readonly, :diff_view
22
+
23
+ def initialize(path:, content:, new_file: false, editor_mode: "modern", readonly: false, diff_view: false)
24
+ @path = path.to_s
25
+ @new_file = new_file
26
+ @readonly = readonly
27
+ @diff_view = diff_view
28
+ @file_marker = EditorFileMarker.new(path: @path, content: content, new_file: new_file)
29
+ @original_content = @file_marker.content
30
+ @original_digest = @file_marker.digest
31
+ @original_mtime = @file_marker.mtime
32
+ @original_size = @file_marker.size
33
+ @text_buffer = EditorBuffer.new(@original_content)
34
+ @buffer = @text_buffer.text
35
+ @cursor = 0
36
+ @viewport_row = 0
37
+ @viewport_column = 0
38
+ @status = nil
39
+ @overwrite_confirmed = false
40
+ @quit_confirmed = false
41
+ @search = EditorSearch.new
42
+ @search_active = @search.active?
43
+ @search_query = @search.query
44
+ @search_direction = @search.direction
45
+ @kill_state = EditorKillRing.new
46
+ @kill_buffer = @kill_state.kill_buffer
47
+ @selections = EditorSelections.new(cursor: @cursor, buffer_length: @buffer.length)
48
+ sync_selection_state
49
+ @editor_mode = normalize_editor_mode(editor_mode)
50
+ @emacs_pending = nil
51
+ @kill_ring = @kill_state.kill_ring
52
+ @last_yank_range = @kill_state.last_yank_range
53
+ @last_yank_index = @kill_state.last_yank_index
54
+ @vibe_state = VibeEditorState.new(editor_mode: @editor_mode)
55
+ sync_vibe_state
56
+ @undo_history = EditorUndoHistory.new
57
+ @undo_stack = @undo_history.undo_stack
58
+ @redo_stack = @undo_history.redo_stack
59
+ @status = default_status
60
+ end
61
+
62
+ def initialize_copy(other)
63
+ super
64
+ @path = other.path.dup
65
+ @original_content = other.original_content.dup
66
+ @file_marker = EditorFileMarker.new(path: @path, content: @original_content, new_file: other.new_file)
67
+ @original_digest = other.original_digest.dup
68
+ @original_mtime = other.original_mtime
69
+ @original_size = other.original_size
70
+ @text_buffer = EditorBuffer.new(other.buffer)
71
+ @buffer = @text_buffer.text
72
+ @status = other.status.dup
73
+ @search = EditorSearch.new(direction: other.search_direction)
74
+ @search_active = other.search_active
75
+ @search_query = other.search_query.dup
76
+ @search_direction = other.search_direction
77
+ @kill_state = EditorKillRing.new(
78
+ kill_buffer: other.kill_buffer.dup,
79
+ kill_ring: other.kill_ring.map(&:dup),
80
+ last_yank_range: other.last_yank_range&.dup,
81
+ last_yank_index: other.last_yank_index
82
+ )
83
+ @kill_buffer = @kill_state.kill_buffer
84
+ @quit_confirmed = other.quit_confirmed
85
+ @viewport_column = other.viewport_column
86
+ @selections = EditorSelections.new(
87
+ cursor: @cursor,
88
+ buffer_length: @buffer.length,
89
+ anchor: other.selection_anchor,
90
+ secondary: other.selections.drop(1).map(&:dup)
91
+ )
92
+ sync_selection_state
93
+ @editor_mode = other.editor_mode.dup
94
+ @emacs_pending = other.emacs_pending&.dup
95
+ @kill_ring = @kill_state.kill_ring
96
+ @last_yank_range = @kill_state.last_yank_range
97
+ @last_yank_index = @kill_state.last_yank_index
98
+ @vibe_state = VibeEditorState.copy(other.vibe_state)
99
+ sync_vibe_state
100
+ @undo_history = EditorUndoHistory.new
101
+ other.undo_stack.each { |entry| @undo_history.undo_stack << duplicate_editor_snapshot(entry) }
102
+ other.redo_stack.each { |entry| @undo_history.redo_stack << duplicate_editor_snapshot(entry) }
103
+ @undo_stack = @undo_history.undo_stack
104
+ @redo_stack = @undo_history.redo_stack
105
+ @readonly = other.readonly
106
+ @diff_view = other.diff_view
107
+ end
108
+
109
+ attr_reader :vibe_state
110
+
111
+ def buffer=(value)
112
+ @text_buffer.text = value
113
+ @buffer = @text_buffer.text
114
+ sync_selection_state if @selections
115
+ end
116
+
117
+ def undo_stack=(value)
118
+ @undo_stack = value
119
+ @undo_history = EditorUndoHistory.new(undo_stack: @undo_stack, redo_stack: @redo_stack)
120
+ end
121
+
122
+ def redo_stack=(value)
123
+ @redo_stack = value
124
+ @undo_history = EditorUndoHistory.new(undo_stack: @undo_stack, redo_stack: @redo_stack)
125
+ end
126
+
127
+ def kill_buffer=(value)
128
+ @kill_state.kill_buffer = value
129
+ sync_kill_state
130
+ end
131
+
132
+ def kill_ring=(value)
133
+ @kill_state.kill_ring = value
134
+ sync_kill_state
135
+ end
136
+
137
+ def last_yank_range=(value)
138
+ @kill_state.last_yank_range = value
139
+ sync_kill_state
140
+ end
141
+
142
+ def last_yank_index=(value)
143
+ @kill_state.last_yank_index = value
144
+ sync_kill_state
145
+ end
146
+
147
+ def vibe_mode
148
+ @vibe_state.mode
149
+ end
150
+
151
+ def vibe_mode=(value)
152
+ @vibe_state.mode = value
153
+ sync_vibe_state
154
+ end
155
+
156
+ def vibe_pending
157
+ @vibe_state.pending
158
+ end
159
+
160
+ def vibe_pending=(value)
161
+ @vibe_state.pending = value
162
+ sync_vibe_state
163
+ end
164
+
165
+ def vibe_command
166
+ @vibe_state.command
167
+ end
168
+
169
+ def vibe_command=(value)
170
+ @vibe_state.command = value
171
+ sync_vibe_state
172
+ end
173
+
174
+ def vibe_last_change
175
+ @vibe_state.last_change
176
+ end
177
+
178
+ def vibe_last_change=(value)
179
+ @vibe_state.last_change = value
180
+ sync_vibe_state
181
+ end
182
+
183
+ def vibe_last_find
184
+ @vibe_state.last_find
185
+ end
186
+
187
+ def vibe_last_find=(value)
188
+ @vibe_state.last_find = value
189
+ sync_vibe_state
190
+ end
191
+
192
+ def vibe_last_visual_selection
193
+ @vibe_state.last_visual_selection
194
+ end
195
+
196
+ def vibe_last_visual_selection=(value)
197
+ @vibe_state.last_visual_selection = value
198
+ sync_vibe_state
199
+ end
200
+
201
+ def vibe_visual_block_insert
202
+ @vibe_state.visual_block_insert
203
+ end
204
+
205
+ def vibe_visual_block_insert=(value)
206
+ @vibe_state.visual_block_insert = value
207
+ sync_vibe_state
208
+ end
209
+
210
+ def vibe_marks
211
+ @vibe_state.marks
212
+ end
213
+
214
+ def vibe_marks=(value)
215
+ @vibe_state.marks = value
216
+ sync_vibe_state
217
+ end
218
+
219
+ def vibe_registers
220
+ @vibe_state.registers
221
+ end
222
+
223
+ def vibe_registers=(value)
224
+ @vibe_state.registers = value
225
+ sync_vibe_state
226
+ end
227
+
228
+ def vibe_macros
229
+ @vibe_state.macros
230
+ end
231
+
232
+ def vibe_macros=(value)
233
+ @vibe_state.macros = value
234
+ sync_vibe_state
235
+ end
236
+
237
+ def vibe_recording_macro
238
+ @vibe_state.recording_macro
239
+ end
240
+
241
+ def vibe_recording_macro=(value)
242
+ @vibe_state.recording_macro = value
243
+ sync_vibe_state
244
+ end
245
+
246
+ def vibe_last_macro
247
+ @vibe_state.last_macro
248
+ end
249
+
250
+ def vibe_last_macro=(value)
251
+ @vibe_state.last_macro = value
252
+ sync_vibe_state
253
+ end
254
+
255
+ def cursor
256
+ @cursor
257
+ end
258
+
259
+ def cursor=(value)
260
+ @cursor = clamp_offset(value)
261
+ @selections.cursor = @cursor
262
+ sync_selection_state
263
+ end
264
+
265
+ def selection_anchor
266
+ @selection_anchor
267
+ end
268
+
269
+ def selection_anchor=(value)
270
+ @selections.anchor = value
271
+ sync_selection_state
272
+ end
273
+
274
+ def selections
275
+ sync_selection_state
276
+ @selections.all
277
+ end
278
+
279
+ def multi_cursor?
280
+ sync_selection_state
281
+ @selections.multi_cursor?
282
+ end
283
+
284
+ def set_selections(values)
285
+ @selections.set(values)
286
+ @cursor = @selections.primary[:cursor]
287
+ sync_selection_state
288
+ end
289
+
290
+ def add_selection(anchor, cursor = anchor)
291
+ @selections.add(anchor, cursor)
292
+ sync_selection_state
293
+ end
294
+
295
+ def collapse_to_primary_selection
296
+ @selections.collapse_to_primary
297
+ sync_selection_state
298
+ end
299
+
300
+ def secondary_cursor_offsets
301
+ sync_selection_state
302
+ @selections.secondary_cursor_offsets
303
+ end
304
+
305
+ def readonly?
306
+ @readonly == true
307
+ end
308
+
309
+ def diff_view?
310
+ @diff_view == true
311
+ end
312
+
313
+ def modern?
314
+ @editor_mode == "modern"
315
+ end
316
+
317
+ def emacs?
318
+ @editor_mode == "emacs"
319
+ end
320
+
321
+ def vibe?
322
+ @editor_mode == "vibe"
323
+ end
324
+
325
+ def dirty?
326
+ @buffer != @original_content
327
+ end
328
+
329
+ def lines
330
+ @text_buffer.lines
331
+ end
332
+
333
+ def cursor_line_and_column
334
+ cursor_line_and_column_for(@cursor)
335
+ end
336
+
337
+ def set_cursor_line_and_column(line_index, column)
338
+ @cursor = offset_for_line_and_column(line_index, column)
339
+ end
340
+
341
+ def offset_for_line_and_column(line_index, column)
342
+ @text_buffer.offset_for_line_and_column(line_index, column)
343
+ end
344
+
345
+ def push_undo
346
+ @undo_history.push(editor_snapshot)
347
+ end
348
+
349
+ def undo
350
+ snapshot = @undo_history.undo(editor_snapshot)
351
+ unless snapshot
352
+ @status = "Already at oldest change"
353
+ return false
354
+ end
355
+
356
+ restore_editor_snapshot(snapshot)
357
+ changed!(clear_selections: false)
358
+ @status = "Undo"
359
+ true
360
+ end
361
+
362
+ def redo
363
+ snapshot = @undo_history.redo(editor_snapshot)
364
+ unless snapshot
365
+ @status = "Already at newest change"
366
+ return false
367
+ end
368
+
369
+ restore_editor_snapshot(snapshot)
370
+ changed!(clear_selections: false)
371
+ @status = "Redo"
372
+ true
373
+ end
374
+
375
+ def insert(text)
376
+ text = text.to_s
377
+ return if text.empty?
378
+ return replace_selections(text) if multi_cursor?
379
+
380
+ @text_buffer.insert(@cursor, text)
381
+ @cursor += text.length
382
+ @buffer = @text_buffer.text
383
+ sync_selection_state
384
+ changed!
385
+ end
386
+
387
+ def delete_before_cursor
388
+ return delete_before_selections if multi_cursor?
389
+ return false if @cursor.zero?
390
+
391
+ @text_buffer.delete_range(@cursor - 1, @cursor)
392
+ @cursor -= 1
393
+ @buffer = @text_buffer.text
394
+ sync_selection_state
395
+ changed!
396
+ true
397
+ end
398
+
399
+ def delete_at_cursor
400
+ return delete_at_selections if multi_cursor?
401
+ return false unless @cursor < @buffer.length
402
+
403
+ @text_buffer.delete_range(@cursor, @cursor + 1)
404
+ @buffer = @text_buffer.text
405
+ sync_selection_state
406
+ changed!
407
+ true
408
+ end
409
+
410
+ def move_left
411
+ if multi_cursor?
412
+ return move_selection_cursors { |selection| [selection[:cursor] - 1, 0].max } if extending_selections?
413
+
414
+ return move_selections { |selection| collapse_or_move_left(selection) }
415
+ end
416
+
417
+ @cursor -= 1 if @cursor.positive?
418
+ end
419
+
420
+ def move_right
421
+ if multi_cursor?
422
+ return move_selection_cursors { |selection| [selection[:cursor] + 1, @buffer.length].min } if extending_selections?
423
+
424
+ return move_selections { |selection| collapse_or_move_right(selection) }
425
+ end
426
+
427
+ @cursor += 1 if @cursor < @buffer.length
428
+ end
429
+
430
+ def move_up
431
+ if multi_cursor?
432
+ return move_selection_cursors { |selection| move_offset_vertically(selection[:cursor], -1) } if extending_selections?
433
+
434
+ return move_selections { |selection| move_offset_vertically(selection[:cursor], -1) }
435
+ end
436
+
437
+ line, column = cursor_line_and_column
438
+ set_cursor_line_and_column(line - 1, column)
439
+ end
440
+
441
+ def move_down
442
+ if multi_cursor?
443
+ return move_selection_cursors { |selection| move_offset_vertically(selection[:cursor], 1) } if extending_selections?
444
+
445
+ return move_selections { |selection| move_offset_vertically(selection[:cursor], 1) }
446
+ end
447
+
448
+ line, column = cursor_line_and_column
449
+ set_cursor_line_and_column(line + 1, column)
450
+ end
451
+
452
+ def move_line_start
453
+ if multi_cursor?
454
+ return move_selection_cursors { |selection| line_start_for_offset(selection[:cursor]) } if extending_selections?
455
+
456
+ return move_selections { |selection| line_start_for_offset(selection[:cursor]) }
457
+ end
458
+
459
+ line, = cursor_line_and_column
460
+ set_cursor_line_and_column(line, 0)
461
+ end
462
+
463
+ def move_line_first_non_blank
464
+ line, = cursor_line_and_column
465
+ move_to_line_first_non_blank(line)
466
+ end
467
+
468
+ def move_to_line_first_non_blank(line_index)
469
+ line = [[line_index.to_i, 0].max, lines.length - 1].min
470
+ column = lines[line].to_s.index(/\S/) || 0
471
+ set_cursor_line_and_column(line, column)
472
+ end
473
+
474
+ def move_line_end
475
+ if multi_cursor?
476
+ return move_selection_cursors { |selection| line_end_for_offset(selection[:cursor]) } if extending_selections?
477
+
478
+ return move_selections { |selection| line_end_for_offset(selection[:cursor]) }
479
+ end
480
+
481
+ line, = cursor_line_and_column
482
+ set_cursor_line_and_column(line, lines[line].length)
483
+ end
484
+
485
+ def page_up(rows)
486
+ if multi_cursor?
487
+ return move_selection_cursors { |selection| move_offset_vertically(selection[:cursor], -rows.to_i) } if extending_selections?
488
+
489
+ return move_selections { |selection| move_offset_vertically(selection[:cursor], -rows.to_i) }
490
+ end
491
+
492
+ line, column = cursor_line_and_column
493
+ set_cursor_line_and_column(line - rows.to_i, column)
494
+ end
495
+
496
+ def page_down(rows)
497
+ if multi_cursor?
498
+ return move_selection_cursors { |selection| move_offset_vertically(selection[:cursor], rows.to_i) } if extending_selections?
499
+
500
+ return move_selections { |selection| move_offset_vertically(selection[:cursor], rows.to_i) }
501
+ end
502
+
503
+ line, column = cursor_line_and_column
504
+ set_cursor_line_and_column(line + rows.to_i, column)
505
+ end
506
+
507
+ def move_to_previous_word
508
+ if multi_cursor?
509
+ return move_selection_cursors { |selection| previous_word_boundary(selection[:cursor]) } if extending_selections?
510
+
511
+ return move_selections { |selection| previous_word_boundary(selection[:cursor]) }
512
+ end
513
+
514
+ @cursor = previous_word_boundary(@cursor)
515
+ end
516
+
517
+ def move_to_next_word
518
+ if multi_cursor?
519
+ return move_selection_cursors { |selection| next_word_boundary(selection[:cursor]) } if extending_selections?
520
+
521
+ return move_selections { |selection| next_word_boundary(selection[:cursor]) }
522
+ end
523
+
524
+ @cursor = next_word_boundary(@cursor)
525
+ end
526
+
527
+ def move_to_word_end
528
+ if multi_cursor?
529
+ return move_selection_cursors { |selection| next_word_end(selection[:cursor]) } if extending_selections?
530
+
531
+ return move_selections { |selection| next_word_end(selection[:cursor]) }
532
+ end
533
+
534
+ @cursor = next_word_end(@cursor)
535
+ end
536
+
537
+ def move_indentation_up
538
+ if multi_cursor?
539
+ return move_selection_cursors { |selection| indentation_offset_for(selection[:cursor], :up) } if extending_selections?
540
+
541
+ return move_selections { |selection| indentation_offset_for(selection[:cursor], :up) }
542
+ end
543
+
544
+ line, column = cursor_line_and_column
545
+ target_line = previous_indentation_line(line, indentation_level_for_line(line))
546
+ move_to_indentation_line(target_line, column)
547
+ end
548
+
549
+ def move_indentation_down
550
+ if multi_cursor?
551
+ return move_selection_cursors { |selection| indentation_offset_for(selection[:cursor], :down) } if extending_selections?
552
+
553
+ return move_selections { |selection| indentation_offset_for(selection[:cursor], :down) }
554
+ end
555
+
556
+ line, column = cursor_line_and_column
557
+ target_line = next_indentation_line(line, indentation_level_for_line(line))
558
+ move_to_indentation_line(target_line, column)
559
+ end
560
+
561
+ def move_indentation_right
562
+ if multi_cursor?
563
+ return move_selection_cursors { |selection| indentation_right_offset_for(selection[:cursor]) } if extending_selections?
564
+
565
+ return move_selections { |selection| indentation_right_offset_for(selection[:cursor]) }
566
+ end
567
+
568
+ line, column = cursor_line_and_column
569
+ indentation = indentation_level_for_line(line)
570
+ if column < indentation
571
+ set_cursor_line_and_column(line, indentation)
572
+ else
573
+ move_to_word_end
574
+ end
575
+ end
576
+
577
+ def delete_word_before_cursor
578
+ return apply_selection_edits { |selection| selection[:anchor] = previous_word_boundary(selection_range_for(selection)[0]); "" } if multi_cursor?
579
+
580
+ kill_range(previous_word_boundary(@cursor), @cursor)
581
+ end
582
+
583
+ def delete_word_after_cursor
584
+ return apply_selection_edits { |selection| selection[:cursor] = next_word_boundary(selection_range_for(selection)[1]); "" } if multi_cursor?
585
+
586
+ kill_range(@cursor, next_word_boundary(@cursor))
587
+ end
588
+
589
+ def kill_line_before_cursor
590
+ return apply_selection_edits { |selection| selection[:anchor] = line_start_for_offset(selection_range_for(selection)[0]); "" } if multi_cursor?
591
+
592
+ kill_range(current_line_start, @cursor)
593
+ end
594
+
595
+ def kill_line_after_cursor
596
+ if multi_cursor?
597
+ return apply_selection_edits do |selection|
598
+ range = selection_range_for(selection)
599
+ selection[:cursor] = line_end_for_offset(range[1])
600
+ selection[:cursor] += 1 if range[0] == selection[:cursor] && selection[:cursor] < @buffer.length
601
+ ""
602
+ end
603
+ end
604
+
605
+ if current_line_empty?
606
+ kill_range(empty_line_start, empty_line_end)
607
+ else
608
+ kill_range(@cursor, current_line_end)
609
+ end
610
+ end
611
+
612
+ def yank_kill_buffer
613
+ replace_selections(@kill_buffer.to_s) unless @kill_buffer.to_s.empty?
614
+ end
615
+
616
+ def push_kill(text)
617
+ return false unless @kill_state.push(text)
618
+
619
+ sync_kill_state
620
+ true
621
+ end
622
+
623
+ def yank_from_kill_ring
624
+ text = @kill_state.first_yank
625
+ return false unless text
626
+
627
+ start_index = @cursor
628
+ insert(text)
629
+ @kill_state.record_yank(start_index, @cursor)
630
+ sync_kill_state
631
+ true
632
+ end
633
+
634
+ def yank_pop
635
+ yank = @kill_state.next_yank_pop
636
+ return false unless yank
637
+
638
+ start_index, end_index = yank[:range]
639
+ text = yank[:text]
640
+ replace_range(start_index, end_index, text)
641
+ @cursor = start_index + text.length
642
+ @kill_state.record_yank_pop(start_index, @cursor)
643
+ sync_kill_state
644
+ true
645
+ end
646
+
647
+ def begin_selection
648
+ self.selection_anchor = @cursor
649
+ @status = "Selection started"
650
+ end
651
+
652
+ def clear_selection
653
+ @selections.clear
654
+ sync_selection_state
655
+ end
656
+
657
+ def selection_active?
658
+ return false if @selection_anchor.nil?
659
+ return true if vibe? && %w[visual visual_line visual_block].include?(@vibe_mode)
660
+
661
+ @selection_anchor != @cursor
662
+ end
663
+
664
+ def selection_range
665
+ return visual_line_selection_range if vibe? && @vibe_mode == "visual_line" && selection_active?
666
+ return visual_character_selection_range if vibe? && @vibe_mode == "visual" && selection_active?
667
+ return visual_block_selection_ranges.first if vibe? && @vibe_mode == "visual_block" && selection_active?
668
+ return nil unless primary_selection_active?
669
+
670
+ [@selection_anchor, @cursor].minmax
671
+ end
672
+
673
+ def selection_ranges
674
+ if vibe? && %w[visual visual_line visual_block].include?(@vibe_mode) && selection_active?
675
+ return visual_block_selection_ranges if @vibe_mode == "visual_block"
676
+
677
+ return [selection_range]
678
+ end
679
+
680
+ selections.filter_map do |selection|
681
+ range = selection_range_for(selection)
682
+ range if range[0] != range[1]
683
+ end
684
+ end
685
+
686
+ def visual_character_selection_range
687
+ start_index, end_index = [@selection_anchor, @cursor].minmax
688
+ [start_index, [end_index + 1, @buffer.length].min]
689
+ end
690
+
691
+ def visual_line_selection_range
692
+ anchor_line, = cursor_line_and_column_for(@selection_anchor)
693
+ cursor_line, = cursor_line_and_column
694
+ start_line, end_line = [anchor_line, cursor_line].minmax
695
+ start_index, = line_range(start_line)
696
+ _, end_index = line_range(end_line)
697
+ [start_index, end_index]
698
+ end
699
+
700
+ def visual_block_selection_ranges
701
+ anchor_line, anchor_column = cursor_line_and_column_for(@selection_anchor)
702
+ cursor_line, cursor_column = cursor_line_and_column
703
+ start_line, end_line = [anchor_line, cursor_line].minmax
704
+ start_column, end_column = [anchor_column, cursor_column].minmax
705
+ (start_line..end_line).map do |line_index|
706
+ line_start = line_start_offset(line_index)
707
+ line_length = lines[line_index].to_s.length
708
+ range_start = line_start + [start_column, line_length].min
709
+ range_end = line_start + [end_column + 1, line_length].min
710
+ [range_start, range_end]
711
+ end
712
+ end
713
+
714
+ def selected_text
715
+ if vibe? && @vibe_mode == "visual_block"
716
+ return selection_ranges.map { |range| @buffer[range[0]...range[1]].to_s }.join("\n")
717
+ end
718
+
719
+ ranges = selection_ranges
720
+ return "" if ranges.empty?
721
+
722
+ ranges.map { |range| @buffer[range[0]...range[1]].to_s }.join("\n")
723
+ end
724
+
725
+ def cursor_line_and_column_for(offset)
726
+ before_cursor = @buffer[0...offset].to_s
727
+ [before_cursor.count("\n"), (before_cursor.split("\n", -1).last || "").length]
728
+ end
729
+
730
+ def line_start_offset(line_index)
731
+ values = lines
732
+ line_index = [[line_index.to_i, 0].max, values.length - 1].min
733
+ values.first(line_index).sum { |line| line.length + 1 }
734
+ end
735
+
736
+ def line_range(line_index)
737
+ start_index = line_start_offset(line_index)
738
+ end_index = start_index + lines[line_index].to_s.length
739
+ end_index += 1 if end_index < @buffer.length
740
+ [start_index, end_index]
741
+ end
742
+
743
+ def word_range_at(offset)
744
+ return nil if @buffer.empty?
745
+
746
+ index = [[offset.to_i, 0].max, @buffer.length - 1].min
747
+ return nil if word_separator?(@buffer[index])
748
+
749
+ start_index = index
750
+ start_index -= 1 while start_index.positive? && !word_separator?(@buffer[start_index - 1])
751
+ end_index = index + 1
752
+ end_index += 1 while end_index < @buffer.length && !word_separator?(@buffer[end_index])
753
+ [start_index, end_index]
754
+ end
755
+
756
+ def current_line_range
757
+ line, = cursor_line_and_column
758
+ line_range(line)
759
+ end
760
+
761
+ def move_file_start
762
+ @cursor = 0
763
+ end
764
+
765
+ def move_file_end
766
+ @cursor = @buffer.length
767
+ end
768
+
769
+ def replace_range(start_index, end_index, text)
770
+ start_index, = @text_buffer.replace_range(start_index, end_index, text)
771
+ @buffer = @text_buffer.text
772
+ @cursor = [start_index, @buffer.length].min
773
+ changed!
774
+ end
775
+
776
+ def replace_selections(text)
777
+ apply_selection_edits { |_selection| text.to_s }
778
+ end
779
+
780
+ def delete_before_selections
781
+ apply_selection_edits do |selection|
782
+ range = selection_range_for(selection)
783
+ if range[0] != range[1]
784
+ ""
785
+ elsif range[0].positive?
786
+ selection[:anchor] = range[0] - 1
787
+ ""
788
+ end
789
+ end
790
+ end
791
+
792
+ def delete_at_selections
793
+ apply_selection_edits do |selection|
794
+ range = selection_range_for(selection)
795
+ if range[0] != range[1]
796
+ ""
797
+ elsif range[1] < @buffer.length
798
+ selection[:cursor] = range[1] + 1
799
+ ""
800
+ end
801
+ end
802
+ end
803
+
804
+ def copy_range(start_index, end_index)
805
+ start_index, end_index = [start_index, end_index].minmax
806
+ self.kill_buffer = @buffer[start_index...end_index].to_s
807
+ end
808
+
809
+ def copy_for_kill_ring(start_index, end_index)
810
+ start_index, end_index = [start_index, end_index].minmax
811
+ push_kill(@buffer[start_index...end_index].to_s)
812
+ end
813
+
814
+ def cut_range(start_index, end_index)
815
+ kill_range(start_index, end_index)
816
+ end
817
+
818
+ def add_next_occurrence_selection
819
+ range = selection_range || word_range_at(@cursor)
820
+ unless range
821
+ @status = "No word under cursor"
822
+ return false
823
+ end
824
+
825
+ query = @buffer[range[0]...range[1]].to_s
826
+ if query.empty?
827
+ @status = "No word under cursor"
828
+ return false
829
+ end
830
+
831
+ existing_ranges = selection_ranges
832
+ existing_ranges = [range] if existing_ranges.empty?
833
+ start_after = existing_ranges.map(&:last).max || range[1]
834
+ if selection_range.nil?
835
+ set_selections([{ anchor: range[0], cursor: range[1] }])
836
+ @status = "Selected: #{query}"
837
+ return true
838
+ end
839
+
840
+ match = next_occurrence_range(query, start_after, existing_ranges)
841
+ unless match
842
+ @status = "No more matches: #{query}"
843
+ return false
844
+ end
845
+
846
+ add_selection(match[0], match[1])
847
+ @status = "Added cursor for: #{query}"
848
+ true
849
+ end
850
+
851
+ def add_vertical_cursor(direction)
852
+ source = direction == :up ? selections.min_by { |selection| selection[:cursor] } : selections.max_by { |selection| selection[:cursor] }
853
+ line, column = cursor_line_and_column_for(source[:cursor])
854
+ target_line = direction == :up ? line - 1 : line + 1
855
+ if target_line.negative? || target_line >= lines.length
856
+ @status = direction == :up ? "No line above" : "No line below"
857
+ return false
858
+ end
859
+
860
+ target_column = [column, lines[target_line].to_s.length].min
861
+ offset = line_start_offset(target_line) + target_column
862
+ add_selection(offset, offset)
863
+ @status = direction == :up ? "Added cursor above" : "Added cursor below"
864
+ true
865
+ end
866
+
867
+ def selection_to_line_start_cursors
868
+ range = selection_range
869
+ unless range
870
+ @status = "No selection"
871
+ return false
872
+ end
873
+
874
+ start_line, = cursor_line_and_column_for(range[0])
875
+ end_line, end_column = cursor_line_and_column_for(range[1])
876
+ end_line -= 1 if end_column.zero? && end_line > start_line
877
+ set_selections((start_line..end_line).map do |line_index|
878
+ offset = line_start_offset(line_index)
879
+ { anchor: offset, cursor: offset }
880
+ end)
881
+ @status = "Created #{selections.length} cursors"
882
+ true
883
+ end
884
+
885
+ def extending_selections
886
+ previous = @extending_selections
887
+ @extending_selections = true
888
+ yield
889
+ ensure
890
+ @extending_selections = previous
891
+ end
892
+
893
+ def begin_search(direction = :forward)
894
+ @status = @search.begin(direction)
895
+ sync_search_state
896
+ true
897
+ end
898
+
899
+ def cancel_search
900
+ @status = @search.cancel
901
+ sync_search_state
902
+ true
903
+ end
904
+
905
+ def append_search(text)
906
+ @status = @search.append(text)
907
+ sync_search_state
908
+ true
909
+ end
910
+
911
+ def delete_search_character
912
+ @status = @search.delete_character
913
+ sync_search_state
914
+ true
915
+ end
916
+
917
+ def confirm_search
918
+ apply_search_result(@search.confirm(buffer: @buffer, cursor: @cursor))
919
+ end
920
+
921
+ def repeat_search(direction = @search_direction, query = @search_query)
922
+ apply_search_result(@search.repeat(buffer: @buffer, cursor: @cursor, direction: direction, query: query))
923
+ end
924
+
925
+ def word_under_cursor
926
+ return "" if @buffer.empty?
927
+
928
+ index = [[@cursor, 0].max, @buffer.length - 1].min
929
+ index -= 1 while index.positive? && word_separator?(@buffer[index])
930
+ return "" if word_separator?(@buffer[index])
931
+
932
+ start_index = index
933
+ start_index -= 1 while start_index.positive? && !word_separator?(@buffer[start_index - 1])
934
+ end_index = index + 1
935
+ end_index += 1 while end_index < @buffer.length && !word_separator?(@buffer[end_index])
936
+ @buffer[start_index...end_index].to_s
937
+ end
938
+
939
+ def refresh_after_save(content)
940
+ @new_file = false
941
+ @file_marker.refresh(content)
942
+ @original_content = @file_marker.content
943
+ @original_digest = @file_marker.digest
944
+ @original_mtime = @file_marker.mtime
945
+ @original_size = @file_marker.size
946
+ @overwrite_confirmed = false
947
+ @quit_confirmed = false
948
+ @status = "Saved #{@path}"
949
+ end
950
+
951
+ def file_changed_on_disk?
952
+ @file_marker.changed_on_disk?(new_file: new_file)
953
+ end
954
+
955
+ private
956
+
957
+ def clamp_offset(value)
958
+ [[value.to_i, 0].max, @buffer.length].min
959
+ end
960
+
961
+ def primary_selection
962
+ sync_selection_state
963
+ @selections.primary
964
+ end
965
+
966
+ def primary_selection_active?
967
+ sync_selection_state
968
+ @selections.primary_active?(vibe_visual: vibe? && %w[visual visual_line visual_block].include?(@vibe_mode))
969
+ end
970
+
971
+ def selection_range_for(selection)
972
+ @selections.range_for(selection)
973
+ end
974
+
975
+ def editor_snapshot
976
+ { buffer: @buffer.dup, selections: selections }
977
+ end
978
+
979
+ def restore_editor_snapshot(snapshot)
980
+ self.buffer = snapshot[:buffer].to_s
981
+ if snapshot[:selections]
982
+ set_selections(snapshot[:selections])
983
+ else
984
+ set_selections([{ anchor: nil, cursor: [snapshot[:cursor].to_i, @buffer.length].min }])
985
+ clear_selection
986
+ end
987
+ end
988
+
989
+ def duplicate_editor_snapshot(snapshot)
990
+ duplicate = { buffer: snapshot[:buffer].to_s.dup }
991
+ if snapshot[:selections]
992
+ duplicate[:selections] = snapshot[:selections].map(&:dup)
993
+ else
994
+ duplicate[:cursor] = snapshot[:cursor]
995
+ end
996
+ duplicate
997
+ end
998
+
999
+ def apply_selection_edits
1000
+ edits = selections.filter_map do |selection|
1001
+ edit_selection = selection.dup
1002
+ replacement = yield(edit_selection)
1003
+ next if replacement.nil?
1004
+
1005
+ range = selection_range_for(edit_selection)
1006
+ { start: range[0], end: range[1], text: replacement.to_s }
1007
+ end
1008
+ return false if edits.empty?
1009
+
1010
+ new_selections = []
1011
+ edits.sort_by { |edit| edit[:start] }.reverse_each do |edit|
1012
+ delta = edit[:text].length - (edit[:end] - edit[:start])
1013
+ new_selections.each do |selection|
1014
+ selection[:anchor] += delta if selection[:anchor] >= edit[:end]
1015
+ selection[:cursor] += delta if selection[:cursor] >= edit[:end]
1016
+ end
1017
+ @text_buffer.replace_range(edit[:start], edit[:end], edit[:text])
1018
+ @buffer = @text_buffer.text
1019
+ sync_selection_state
1020
+ cursor = edit[:start] + edit[:text].length
1021
+ new_selections << { anchor: cursor, cursor: cursor }
1022
+ end
1023
+ changed!(clear_selections: false)
1024
+ set_selections(new_selections.sort_by { |selection| [selection[:cursor], selection[:anchor]] })
1025
+ true
1026
+ end
1027
+
1028
+ def next_occurrence_range(query, start_after, existing_ranges)
1029
+ ranges = occurrence_ranges(query, start_after) + occurrence_ranges(query, 0, limit: start_after)
1030
+ ranges.find { |range| existing_ranges.none? { |existing| existing == range } }
1031
+ end
1032
+
1033
+ def occurrence_ranges(query, start_at, limit: @buffer.length)
1034
+ ranges = []
1035
+ index = @buffer.index(query, start_at)
1036
+ while index && index < limit
1037
+ range = [index, index + query.length]
1038
+ ranges << range if range[1] <= limit
1039
+ index = @buffer.index(query, index + query.length)
1040
+ end
1041
+ ranges
1042
+ end
1043
+
1044
+ def kill_range(start_index, end_index)
1045
+ return false if start_index == end_index
1046
+
1047
+ push_kill(@buffer[start_index...end_index].to_s)
1048
+ @text_buffer.delete_range(start_index, end_index)
1049
+ @buffer = @text_buffer.text
1050
+ @cursor = start_index
1051
+ sync_selection_state
1052
+ changed!
1053
+ true
1054
+ end
1055
+
1056
+ def current_line_start
1057
+ line_start_for_offset(@cursor)
1058
+ end
1059
+
1060
+ def current_line_end
1061
+ line_end_for_offset(@cursor)
1062
+ end
1063
+
1064
+ def line_start_for_offset(offset)
1065
+ @buffer.rindex("\n", offset.to_i - 1)&.+(1) || 0
1066
+ end
1067
+
1068
+ def line_end_for_offset(offset)
1069
+ @buffer.index("\n", offset.to_i) || @buffer.length
1070
+ end
1071
+
1072
+ def move_selections
1073
+ set_selections(selections.map do |selection|
1074
+ offset = clamp_offset(yield(selection))
1075
+ { anchor: offset, cursor: offset }
1076
+ end)
1077
+ end
1078
+
1079
+ def move_selection_cursors
1080
+ set_selections(selections.map do |selection|
1081
+ { anchor: selection[:anchor], cursor: clamp_offset(yield(selection)) }
1082
+ end)
1083
+ end
1084
+
1085
+ def extending_selections?
1086
+ @extending_selections == true
1087
+ end
1088
+
1089
+ def collapse_or_move_left(selection)
1090
+ range = selection_range_for(selection)
1091
+ return range[0] if range[0] != range[1]
1092
+
1093
+ [selection[:cursor] - 1, 0].max
1094
+ end
1095
+
1096
+ def collapse_or_move_right(selection)
1097
+ range = selection_range_for(selection)
1098
+ return range[1] if range[0] != range[1]
1099
+
1100
+ [selection[:cursor] + 1, @buffer.length].min
1101
+ end
1102
+
1103
+ def move_offset_vertically(offset, rows)
1104
+ line, column = cursor_line_and_column_for(offset)
1105
+ offset_for_line_and_column(line + rows.to_i, column)
1106
+ end
1107
+
1108
+ def indentation_offset_for(offset, direction)
1109
+ line, column = cursor_line_and_column_for(offset)
1110
+ target_line = if direction == :up
1111
+ previous_indentation_line(line, indentation_level_for_line(line))
1112
+ else
1113
+ next_indentation_line(line, indentation_level_for_line(line))
1114
+ end
1115
+ return offset if target_line.nil?
1116
+
1117
+ offset_for_line_and_column(target_line, column)
1118
+ end
1119
+
1120
+ def indentation_right_offset_for(offset)
1121
+ line, column = cursor_line_and_column_for(offset)
1122
+ indentation = indentation_level_for_line(line)
1123
+ return offset_for_line_and_column(line, indentation) if column < indentation
1124
+
1125
+ next_word_end(offset)
1126
+ end
1127
+
1128
+ def current_line_empty?
1129
+ current_line_start == @cursor && current_line_end == @cursor
1130
+ end
1131
+
1132
+ def empty_line_start
1133
+ current_line_end == @buffer.length && @cursor.positive? ? @cursor - 1 : @cursor
1134
+ end
1135
+
1136
+ def empty_line_end
1137
+ current_line_end < @buffer.length ? current_line_end + 1 : current_line_end
1138
+ end
1139
+
1140
+ def previous_word_boundary(index)
1141
+ TextBoundary.previous_word_boundary(@buffer, index)
1142
+ end
1143
+
1144
+ def next_word_boundary(index)
1145
+ TextBoundary.next_word_boundary(@buffer, index)
1146
+ end
1147
+
1148
+ def next_word_end(index)
1149
+ return 0 if @buffer.empty?
1150
+
1151
+ cursor = [[index.to_i, 0].max, @buffer.length - 1].min
1152
+ cursor += 1 if cursor < @buffer.length - 1 && !word_separator?(@buffer[cursor])
1153
+ cursor += 1 while cursor < @buffer.length && word_separator?(@buffer[cursor])
1154
+ cursor += 1 while cursor < @buffer.length - 1 && !word_separator?(@buffer[cursor + 1])
1155
+ cursor
1156
+ end
1157
+
1158
+ def indentation_level_for_line(line_index)
1159
+ indent_navigation.indentation_level_for_line(line_index)
1160
+ end
1161
+
1162
+ def empty_line?(line_index)
1163
+ indent_navigation.empty_line?(line_index)
1164
+ end
1165
+
1166
+ def move_to_indentation_line(line_index, column)
1167
+ return if line_index.nil?
1168
+
1169
+ set_cursor_line_and_column(line_index, column)
1170
+ end
1171
+
1172
+ def next_indentation_line(current_line, current_indentation)
1173
+ indent_navigation.next_line(current_line, current_indentation)
1174
+ end
1175
+
1176
+ def previous_indentation_line(current_line, current_indentation)
1177
+ indent_navigation.previous_line(current_line, current_indentation)
1178
+ end
1179
+
1180
+ def indent_navigation
1181
+ EditorIndentNavigation.new(lines)
1182
+ end
1183
+
1184
+ def word_separator?(char)
1185
+ TextBoundary.word_separator?(char)
1186
+ end
1187
+
1188
+ def sync_kill_state
1189
+ @kill_buffer = @kill_state.kill_buffer
1190
+ @kill_ring = @kill_state.kill_ring
1191
+ @last_yank_range = @kill_state.last_yank_range
1192
+ @last_yank_index = @kill_state.last_yank_index
1193
+ end
1194
+
1195
+ def sync_selection_state
1196
+ @selections.buffer_length = @buffer.length
1197
+ @selections.cursor = @cursor
1198
+ @selection_anchor = @selections.anchor
1199
+ end
1200
+
1201
+ def sync_search_state
1202
+ @search_active = @search.active?
1203
+ @search_query = @search.query
1204
+ @search_direction = @search.direction
1205
+ end
1206
+
1207
+ def sync_vibe_state
1208
+ @vibe_mode = @vibe_state.mode
1209
+ @vibe_pending = @vibe_state.pending
1210
+ @vibe_command = @vibe_state.command
1211
+ @vibe_last_change = @vibe_state.last_change
1212
+ @vibe_last_find = @vibe_state.last_find
1213
+ @vibe_last_visual_selection = @vibe_state.last_visual_selection
1214
+ @vibe_visual_block_insert = @vibe_state.visual_block_insert
1215
+ @vibe_marks = @vibe_state.marks
1216
+ @vibe_registers = @vibe_state.registers
1217
+ @vibe_macros = @vibe_state.macros
1218
+ @vibe_recording_macro = @vibe_state.recording_macro
1219
+ @vibe_last_macro = @vibe_state.last_macro
1220
+ end
1221
+
1222
+ def apply_search_result(result)
1223
+ @cursor = result[:cursor] if result[:cursor]
1224
+ @status = result[:status]
1225
+ sync_search_state
1226
+ result[:found]
1227
+ end
1228
+
1229
+ def normalize_editor_mode(value)
1230
+ EditorMode.normalize(value)
1231
+ end
1232
+
1233
+ def default_status
1234
+ EditorStatusText.default(readonly: readonly?, editor_mode: @editor_mode)
1235
+ end
1236
+
1237
+ def changed!(clear_selections: true)
1238
+ @overwrite_confirmed = false
1239
+ @quit_confirmed = false
1240
+ if clear_selections
1241
+ @selections.clear
1242
+ sync_selection_state
1243
+ end
1244
+ @kill_state.clear_last_yank
1245
+ sync_kill_state
1246
+ end
1247
+ end
1248
+ end
1249
+ end