kward 0.71.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -0,0 +1,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
|