kward 0.73.1 → 0.75.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 +51 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -2
- data/Rakefile +18 -0
- data/doc/configuration.md +70 -4
- data/doc/extensibility.md +6 -19
- data/doc/getting-started.md +1 -1
- data/doc/lifecycle-hooks.md +424 -0
- data/doc/mcp.md +72 -0
- data/doc/memory.md +1 -1
- data/doc/personas.md +2 -2
- data/doc/plugins.md +1 -1
- data/doc/releasing.md +7 -15
- data/doc/rpc.md +108 -15
- data/doc/session-management.md +9 -9
- data/doc/skills.md +199 -0
- data/doc/usage.md +14 -7
- data/lib/kward/agent.rb +72 -22
- data/lib/kward/auth/anthropic_oauth.rb +5 -87
- data/lib/kward/auth/oauth_helpers.rb +105 -0
- data/lib/kward/auth/openai_oauth.rb +4 -86
- data/lib/kward/cli/commands.rb +12 -2
- data/lib/kward/cli/compaction.rb +7 -0
- data/lib/kward/cli/doctor.rb +18 -10
- data/lib/kward/cli/git.rb +30 -3
- data/lib/kward/cli/hook_commands.rb +258 -0
- data/lib/kward/cli/interactive_turn.rb +9 -3
- data/lib/kward/cli/memory_commands.rb +1 -5
- data/lib/kward/cli/plugins.rb +41 -1
- data/lib/kward/cli/prompt_interface.rb +32 -3
- data/lib/kward/cli/rendering.rb +1 -1
- data/lib/kward/cli/runtime_helpers.rb +44 -5
- data/lib/kward/cli/sessions.rb +13 -0
- data/lib/kward/cli/settings.rb +226 -152
- data/lib/kward/cli/slash_commands.rb +81 -42
- data/lib/kward/cli/tabs.rb +32 -15
- data/lib/kward/cli.rb +64 -4
- data/lib/kward/cli_transcript_formatter.rb +0 -7
- data/lib/kward/compaction/token_estimator.rb +112 -0
- data/lib/kward/compactor.rb +42 -116
- data/lib/kward/config_files.rb +152 -27
- data/lib/kward/deep_copy.rb +42 -0
- data/lib/kward/diff_view_mode.rb +36 -0
- data/lib/kward/events.rb +1 -0
- data/lib/kward/export_path.rb +2 -6
- data/lib/kward/frontmatter.rb +43 -0
- data/lib/kward/hooks/audit_log.rb +121 -0
- data/lib/kward/hooks/catalog.rb +110 -0
- data/lib/kward/hooks/command_handler.rb +66 -0
- data/lib/kward/hooks/config_loader.rb +104 -0
- data/lib/kward/hooks/decision.rb +102 -0
- data/lib/kward/hooks/event.rb +60 -0
- data/lib/kward/hooks/http_handler.rb +72 -0
- data/lib/kward/hooks/manager.rb +158 -0
- data/lib/kward/hooks/matcher.rb +75 -0
- data/lib/kward/hooks.rb +16 -0
- data/lib/kward/markdown_transcript.rb +4 -3
- data/lib/kward/mcp/client.rb +56 -0
- data/lib/kward/mcp/server_config.rb +59 -0
- data/lib/kward/mcp/stdio_transport.rb +112 -0
- data/lib/kward/memory/turn_context.rb +19 -0
- data/lib/kward/message_access.rb +2 -2
- data/lib/kward/message_text.rb +19 -0
- data/lib/kward/model/client.rb +13 -45
- data/lib/kward/model/context_usage.rb +10 -4
- data/lib/kward/model/copilot_models.rb +44 -0
- data/lib/kward/model/model_info.rb +24 -0
- data/lib/kward/model/payloads.rb +15 -17
- data/lib/kward/model/stream_parser.rb +178 -13
- data/lib/kward/pan/server.rb +51 -2
- data/lib/kward/path_guard.rb +13 -0
- data/lib/kward/plugin_registry.rb +86 -25
- data/lib/kward/prompt_interface/editor/controller.rb +124 -4
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +10 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +448 -20
- data/lib/kward/prompt_interface/editor/renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/search.rb +70 -13
- data/lib/kward/prompt_interface/editor/state.rb +72 -13
- data/lib/kward/prompt_interface/editor/vibe_state.rb +12 -1
- data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
- data/lib/kward/prompt_interface.rb +4 -1
- data/lib/kward/prompts/commands.rb +3 -3
- data/lib/kward/prompts.rb +7 -7
- data/lib/kward/rpc/mcp_status.rb +50 -0
- data/lib/kward/rpc/prompt_bridge.rb +38 -4
- data/lib/kward/rpc/runtime_payloads.rb +1 -1
- data/lib/kward/rpc/server.rb +94 -13
- data/lib/kward/rpc/session_manager.rb +258 -33
- data/lib/kward/rpc/session_metrics.rb +1 -1
- data/lib/kward/rpc/session_tree_rows.rb +13 -64
- data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
- data/lib/kward/rpc/transcript_normalizer.rb +9 -10
- data/lib/kward/rpc/turn_context.rb +109 -0
- data/lib/kward/session_store.rb +6 -20
- data/lib/kward/session_tree_nodes.rb +98 -18
- data/lib/kward/session_tree_renderer.rb +6 -61
- data/lib/kward/skills/registry.rb +109 -17
- data/lib/kward/tools/mcp_tool.rb +122 -0
- data/lib/kward/tools/read_skill.rb +9 -4
- data/lib/kward/tools/registry.rb +297 -9
- data/lib/kward/tools/search/code.rb +4 -22
- data/lib/kward/tools/search/web.rb +28 -2
- data/lib/kward/update_check.rb +117 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/manager.rb +43 -3
- data/lib/kward/workspace.rb +3 -1
- data/templates/default/fulldoc/html/css/kward.css +22 -0
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +4 -2
- data/templates/default/layout/html/setup.rb +2 -0
- metadata +30 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative "config_files"
|
|
2
|
+
require_relative "deep_copy"
|
|
3
|
+
require_relative "hooks"
|
|
2
4
|
|
|
3
5
|
# Namespace for the Kward CLI agent runtime.
|
|
4
6
|
module Kward
|
|
@@ -39,6 +41,9 @@ module Kward
|
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# Registered lifecycle hook handler.
|
|
45
|
+
HookHandler = Struct.new(:event, :id, :description, :path, :order, :match, :failure_policy, :handler, keyword_init: true)
|
|
46
|
+
|
|
42
47
|
# Read-only transcript view exposed to plugin code.
|
|
43
48
|
class Transcript
|
|
44
49
|
# Creates an object for trusted plugin loading and dispatch.
|
|
@@ -50,7 +55,7 @@ module Kward
|
|
|
50
55
|
#
|
|
51
56
|
# @return [Array<Hash>] immutable transcript message data
|
|
52
57
|
def messages
|
|
53
|
-
|
|
58
|
+
DeepCopy.freeze(DeepCopy.dup(@conversation.messages))
|
|
54
59
|
end
|
|
55
60
|
end
|
|
56
61
|
|
|
@@ -102,6 +107,34 @@ module Kward
|
|
|
102
107
|
@conversation.refresh_system_message! if @conversation.respond_to?(:refresh_system_message!)
|
|
103
108
|
nil
|
|
104
109
|
end
|
|
110
|
+
|
|
111
|
+
def allow(message = nil, metadata: nil)
|
|
112
|
+
Hooks::Decision.allow(message, metadata: metadata)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def deny(message = nil, metadata: nil)
|
|
116
|
+
Hooks::Decision.deny(message, metadata: metadata)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ask(message = nil, metadata: nil)
|
|
120
|
+
Hooks::Decision.ask(message, metadata: metadata)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def modify(payload, message: nil, metadata: nil)
|
|
124
|
+
Hooks::Decision.modify(payload, message: message, metadata: metadata)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def warn(message = nil, metadata: nil)
|
|
128
|
+
Hooks::Decision.warn(message, metadata: metadata)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def retry(message = nil, payload: nil, metadata: nil)
|
|
132
|
+
Hooks::Decision.retry(message, payload: payload, metadata: metadata)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def defer(message = nil, payload: nil, metadata: nil)
|
|
136
|
+
Hooks::Decision.defer(message, payload: payload, metadata: metadata)
|
|
137
|
+
end
|
|
105
138
|
end
|
|
106
139
|
|
|
107
140
|
# Public DSL object yielded by `Kward.plugin` blocks.
|
|
@@ -165,6 +198,25 @@ module Kward
|
|
|
165
198
|
@registry.register_transcript_event(path: @path, &block)
|
|
166
199
|
end
|
|
167
200
|
|
|
201
|
+
# Registers a lifecycle hook handler.
|
|
202
|
+
#
|
|
203
|
+
# Hooks are deterministic runtime callbacks around Kward lifecycle events.
|
|
204
|
+
# They can return a {Hooks::Decision}, a decision hash, a decision string,
|
|
205
|
+
# or nil to allow the operation.
|
|
206
|
+
#
|
|
207
|
+
# @param event [String, #to_s] lifecycle event name
|
|
208
|
+
# @param id [String, nil] stable hook identifier for logs and diagnostics
|
|
209
|
+
# @param description [String] short human-readable purpose
|
|
210
|
+
# @param order [Integer] lower values run first
|
|
211
|
+
# @param match [Hash, nil] optional event selector
|
|
212
|
+
# @yieldparam event [Hooks::Event] immutable lifecycle event
|
|
213
|
+
# @yieldparam ctx [Context] plugin execution context and decision helpers
|
|
214
|
+
# @return [void]
|
|
215
|
+
# @api public
|
|
216
|
+
def hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, &block)
|
|
217
|
+
@registry.register_hook(event, id: id, description: description, order: order, match: match, failure_policy: failure_policy, path: @path, &block)
|
|
218
|
+
end
|
|
219
|
+
|
|
168
220
|
# Registers prompt context text injected into future system prompts.
|
|
169
221
|
#
|
|
170
222
|
# Keep this text short and never include secrets. The returned string can
|
|
@@ -205,29 +257,6 @@ module Kward
|
|
|
205
257
|
paths.each { |path| registry.load_file(path) }
|
|
206
258
|
registry
|
|
207
259
|
end
|
|
208
|
-
|
|
209
|
-
def deep_dup(value)
|
|
210
|
-
case value
|
|
211
|
-
when Hash
|
|
212
|
-
value.each_with_object({}) { |(key, item), result| result[key] = deep_dup(item) }
|
|
213
|
-
when Array
|
|
214
|
-
value.map { |item| deep_dup(item) }
|
|
215
|
-
else
|
|
216
|
-
value.dup
|
|
217
|
-
end
|
|
218
|
-
rescue TypeError
|
|
219
|
-
value
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def deep_freeze(value)
|
|
223
|
-
case value
|
|
224
|
-
when Hash
|
|
225
|
-
value.each_value { |item| deep_freeze(item) }
|
|
226
|
-
when Array
|
|
227
|
-
value.each { |item| deep_freeze(item) }
|
|
228
|
-
end
|
|
229
|
-
value.freeze
|
|
230
|
-
end
|
|
231
260
|
end
|
|
232
261
|
|
|
233
262
|
# Creates an object for trusted plugin loading and dispatch.
|
|
@@ -239,6 +268,7 @@ module Kward
|
|
|
239
268
|
@footer_path = nil
|
|
240
269
|
@transcript_event_handlers = []
|
|
241
270
|
@prompt_context_renderers = []
|
|
271
|
+
@hook_handlers = []
|
|
242
272
|
@paths = []
|
|
243
273
|
end
|
|
244
274
|
|
|
@@ -276,6 +306,20 @@ module Kward
|
|
|
276
306
|
@prompt_context_renderers.map { |entry| entry[:renderer] }
|
|
277
307
|
end
|
|
278
308
|
|
|
309
|
+
def hook_handlers
|
|
310
|
+
@hook_handlers.dup
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def hook_manager
|
|
314
|
+
manager = Hooks::Manager.new
|
|
315
|
+
@hook_handlers.each do |hook|
|
|
316
|
+
manager.register(hook.event, id: hook.id, source: hook.path, order: hook.order, match: hook.match, failure_policy: hook.failure_policy) do |event, context|
|
|
317
|
+
hook.handler.call(event, context)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
manager
|
|
321
|
+
end
|
|
322
|
+
|
|
279
323
|
def prompt_context(context)
|
|
280
324
|
parts = []
|
|
281
325
|
@prompt_context_renderers.each do |entry|
|
|
@@ -387,6 +431,23 @@ module Kward
|
|
|
387
431
|
@prompt_context_renderers << { path: path, renderer: renderer }
|
|
388
432
|
end
|
|
389
433
|
|
|
434
|
+
def register_hook(event, id: nil, description: "", order: 100, match: nil, failure_policy: nil, path: nil, &handler)
|
|
435
|
+
event = event.to_s
|
|
436
|
+
raise "Plugin hook event is required" if event.empty?
|
|
437
|
+
raise "Plugin hook #{event} requires a handler" unless handler
|
|
438
|
+
|
|
439
|
+
@hook_handlers << HookHandler.new(
|
|
440
|
+
event: event,
|
|
441
|
+
id: id&.to_s || "#{File.basename(path.to_s.empty? ? "plugin" : path)}:#{event}:#{@hook_handlers.length + 1}",
|
|
442
|
+
description: description.to_s,
|
|
443
|
+
path: path,
|
|
444
|
+
order: order.to_i,
|
|
445
|
+
match: match,
|
|
446
|
+
failure_policy: failure_policy,
|
|
447
|
+
handler: handler
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
|
|
390
451
|
private
|
|
391
452
|
|
|
392
453
|
def transcript_event_for(event)
|
|
@@ -422,7 +483,7 @@ module Kward
|
|
|
422
483
|
def transcript_event(type, payload)
|
|
423
484
|
TranscriptEvent.new(
|
|
424
485
|
type: type,
|
|
425
|
-
payload:
|
|
486
|
+
payload: DeepCopy.freeze(DeepCopy.dup(payload))
|
|
426
487
|
).freeze
|
|
427
488
|
end
|
|
428
489
|
end
|
|
@@ -112,7 +112,9 @@ module Kward
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def open_diff_viewer(path, content)
|
|
115
|
-
|
|
115
|
+
diff_view_mode = current_diff_view_mode
|
|
116
|
+
viewer_content = diff_view_mode == DiffViewMode::SIDE_BY_SIDE ? side_by_side_diff_content(content.to_s) : content.to_s
|
|
117
|
+
@editor_state = EditorState.new(path: path.to_s, content: viewer_content, new_file: true, editor_mode: current_editor_mode, readonly: true, diff_view: diff_view_mode)
|
|
116
118
|
@prompt_label = "Diff>"
|
|
117
119
|
self.composer_input = ""
|
|
118
120
|
self.composer_cursor = 0
|
|
@@ -131,6 +133,63 @@ module Kward
|
|
|
131
133
|
@editor_mode
|
|
132
134
|
end
|
|
133
135
|
|
|
136
|
+
def current_diff_view_mode
|
|
137
|
+
configured = @diff_view_source.respond_to?(:call) ? @diff_view_source.call : @diff_view
|
|
138
|
+
DiffViewMode.resolve(configured, terminal_width: screen_width)
|
|
139
|
+
rescue StandardError
|
|
140
|
+
DiffViewMode.resolve(@diff_view, terminal_width: screen_width)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def side_by_side_diff_content(content)
|
|
144
|
+
width = side_by_side_diff_column_width
|
|
145
|
+
rows = []
|
|
146
|
+
pending_deletions = []
|
|
147
|
+
|
|
148
|
+
content.to_s.each_line(chomp: true) do |line|
|
|
149
|
+
if side_by_side_diff_metadata_line?(line)
|
|
150
|
+
rows.concat(flush_side_by_side_deletions(pending_deletions, width))
|
|
151
|
+
rows << line
|
|
152
|
+
elsif line.start_with?("-")
|
|
153
|
+
pending_deletions << line[1..].to_s
|
|
154
|
+
elsif line.start_with?("+")
|
|
155
|
+
deleted = pending_deletions.shift
|
|
156
|
+
rows << side_by_side_diff_row(deleted && "- #{deleted}", "+ #{line[1..]}", width)
|
|
157
|
+
else
|
|
158
|
+
rows.concat(flush_side_by_side_deletions(pending_deletions, width))
|
|
159
|
+
text = line.start_with?(" ") ? line[1..].to_s : line
|
|
160
|
+
rows << side_by_side_diff_row(" #{text}", " #{text}", width)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
rows.concat(flush_side_by_side_deletions(pending_deletions, width))
|
|
165
|
+
rows.join("\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def side_by_side_diff_metadata_line?(line)
|
|
169
|
+
line.start_with?("diff --git", "index ", "---", "+++", "@@")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def flush_side_by_side_deletions(lines, width)
|
|
173
|
+
lines.shift(lines.length).map { |line| side_by_side_diff_row("- #{line}", nil, width) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def side_by_side_diff_row(left, right, width)
|
|
177
|
+
left_text = side_by_side_diff_cell(left, width)
|
|
178
|
+
right_text = side_by_side_diff_cell(right, width)
|
|
179
|
+
"#{left_text.ljust(width)} │ #{right_text}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def side_by_side_diff_cell(text, width)
|
|
183
|
+
value = text.nil? ? "" : text.to_s
|
|
184
|
+
visible_truncate(value, width)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def side_by_side_diff_column_width
|
|
188
|
+
content_width = [screen_width - 4, 1].max
|
|
189
|
+
gutter_width = [[999.to_s.length, 4].max + 3, 1].max
|
|
190
|
+
[[(content_width - gutter_width - 3) / 2, 10].max, 80].min
|
|
191
|
+
end
|
|
192
|
+
|
|
134
193
|
def current_editor_soft_wrap?
|
|
135
194
|
return @editor_soft_wrap_source.call != false if @editor_soft_wrap_source.respond_to?(:call)
|
|
136
195
|
|
|
@@ -279,6 +338,34 @@ module Kward
|
|
|
279
338
|
end
|
|
280
339
|
end
|
|
281
340
|
|
|
341
|
+
def handle_editor_search_csi_u_key(sequence, enter: :confirm, restore_cursor_on_cancel: true)
|
|
342
|
+
result = case sequence[:code]
|
|
343
|
+
when 13
|
|
344
|
+
if enter == :repeat
|
|
345
|
+
shift_modifier?(sequence[:modifier]) ? editor_search_repeat(:backward) : editor_search_repeat(:forward)
|
|
346
|
+
else
|
|
347
|
+
editor_search_confirm
|
|
348
|
+
end
|
|
349
|
+
when 27
|
|
350
|
+
editor_search_cancel(restore_cursor: restore_cursor_on_cancel)
|
|
351
|
+
when 8, 127
|
|
352
|
+
editor_search_delete_character
|
|
353
|
+
else
|
|
354
|
+
normalized_code = ctrl_code(sequence[:code])
|
|
355
|
+
if ctrl_modifier?(sequence[:modifier]) && normalized_code == 103
|
|
356
|
+
shift_modifier?(sequence[:modifier]) ? editor_search_repeat(:backward) : editor_search_repeat(:forward)
|
|
357
|
+
elsif (text = csi_u_printable_text(sequence))
|
|
358
|
+
editor_search_append(text)
|
|
359
|
+
elsif csi_u_text_field?(sequence)
|
|
360
|
+
true
|
|
361
|
+
else
|
|
362
|
+
false
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
queue_pending_keys(sequence[:remaining]) if result != false && sequence[:remaining] && !sequence[:remaining].empty?
|
|
366
|
+
result
|
|
367
|
+
end
|
|
368
|
+
|
|
282
369
|
def handle_editor_mouse_key(key)
|
|
283
370
|
event = parse_editor_mouse_key(key)
|
|
284
371
|
return false unless event
|
|
@@ -902,32 +989,40 @@ module Kward
|
|
|
902
989
|
|
|
903
990
|
def editor_search_begin(direction = :forward)
|
|
904
991
|
@editor_state.begin_search(direction)
|
|
992
|
+
ensure_editor_cursor_visible
|
|
905
993
|
true
|
|
906
994
|
end
|
|
907
995
|
|
|
908
996
|
def editor_search_append(text)
|
|
909
997
|
@editor_state.append_search(text)
|
|
998
|
+
ensure_editor_cursor_visible
|
|
910
999
|
true
|
|
911
1000
|
end
|
|
912
1001
|
|
|
913
1002
|
def editor_search_delete_character
|
|
914
1003
|
@editor_state.delete_search_character
|
|
1004
|
+
ensure_editor_cursor_visible
|
|
915
1005
|
true
|
|
916
1006
|
end
|
|
917
1007
|
|
|
918
|
-
def editor_search_confirm
|
|
1008
|
+
def editor_search_confirm(clear_highlights: true)
|
|
919
1009
|
@editor_state.confirm_search
|
|
1010
|
+
@editor_state.clear_search_highlights if clear_highlights
|
|
1011
|
+
ensure_editor_cursor_visible
|
|
920
1012
|
true
|
|
921
1013
|
end
|
|
922
1014
|
|
|
923
|
-
def editor_search_cancel
|
|
924
|
-
@editor_state.cancel_search
|
|
1015
|
+
def editor_search_cancel(restore_cursor: true)
|
|
1016
|
+
@editor_state.cancel_search(restore_cursor: restore_cursor)
|
|
1017
|
+
ensure_editor_cursor_visible
|
|
925
1018
|
true
|
|
926
1019
|
end
|
|
927
1020
|
|
|
928
1021
|
def editor_search_repeat(direction = nil)
|
|
929
1022
|
direction ||= @editor_state.search_direction
|
|
930
1023
|
@editor_state.repeat_search(direction)
|
|
1024
|
+
@editor_state.clear_search_highlights unless @editor_state.search_active
|
|
1025
|
+
ensure_editor_cursor_visible
|
|
931
1026
|
true
|
|
932
1027
|
end
|
|
933
1028
|
|
|
@@ -939,6 +1034,31 @@ module Kward
|
|
|
939
1034
|
end
|
|
940
1035
|
|
|
941
1036
|
@editor_state.repeat_search(direction, query)
|
|
1037
|
+
@editor_state.clear_search_highlights
|
|
1038
|
+
ensure_editor_cursor_visible
|
|
1039
|
+
true
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
def ensure_editor_cursor_visible
|
|
1043
|
+
return false unless @editor_state
|
|
1044
|
+
|
|
1045
|
+
content_width = [screen_width - 4, 1].max
|
|
1046
|
+
visible_count = editor_visible_line_count
|
|
1047
|
+
line_index, column = @editor_state.cursor_line_and_column
|
|
1048
|
+
gutter_width = editor_line_number_gutter_width
|
|
1049
|
+
text_width = editor_text_width(content_width, gutter_width)
|
|
1050
|
+
sync_editor_wrap_state(text_width)
|
|
1051
|
+
|
|
1052
|
+
if current_editor_soft_wrap?
|
|
1053
|
+
cursor_visual_row = editor_visual_row_for(line_index, column, text_width)
|
|
1054
|
+
@editor_state.viewport_row = [[@editor_state.viewport_row, cursor_visual_row - visible_count + 1].max, cursor_visual_row].min
|
|
1055
|
+
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
1056
|
+
else
|
|
1057
|
+
@editor_state.viewport_row = [[@editor_state.viewport_row, line_index - visible_count + 1].max, line_index].min
|
|
1058
|
+
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
1059
|
+
@editor_state.viewport_column = [[@editor_state.viewport_column.to_i, column - text_width + 1].max, column].min
|
|
1060
|
+
@editor_state.viewport_column = [@editor_state.viewport_column, 0].max
|
|
1061
|
+
end
|
|
942
1062
|
true
|
|
943
1063
|
end
|
|
944
1064
|
|
|
@@ -112,6 +112,10 @@ module Kward
|
|
|
112
112
|
def handle_emacs_csi_u_key(key)
|
|
113
113
|
sequence = parse_csi_u_key(key)
|
|
114
114
|
return false unless sequence
|
|
115
|
+
if editor_search_active?
|
|
116
|
+
search_result = handle_editor_search_csi_u_key(sequence)
|
|
117
|
+
return search_result unless search_result == false
|
|
118
|
+
end
|
|
115
119
|
|
|
116
120
|
code = sequence[:code]
|
|
117
121
|
modifier = sequence[:modifier]
|
|
@@ -37,16 +37,18 @@ module Kward
|
|
|
37
37
|
|
|
38
38
|
case key
|
|
39
39
|
when "\n", "\r"
|
|
40
|
-
return
|
|
40
|
+
return editor_search_repeat(:forward) if editor_search_active?
|
|
41
41
|
modern_record_undo { modern_insert_text("\n") }
|
|
42
42
|
when "\t"
|
|
43
43
|
modern_record_undo { editor_insert_tab unless editor_search_active? }
|
|
44
44
|
when "\b", "\x7F"
|
|
45
45
|
editor_search_active? ? editor_search_delete_character : modern_record_undo { modern_delete_before_cursor }
|
|
46
|
+
when "\a"
|
|
47
|
+
return editor_search_repeat(:forward) if editor_search_active?
|
|
46
48
|
when TerminalKeys::CTRL_C
|
|
47
49
|
return editor_search_cancel if editor_search_active?
|
|
48
50
|
when "\e"
|
|
49
|
-
return editor_search_cancel if editor_search_active?
|
|
51
|
+
return editor_search_cancel(restore_cursor: false) if editor_search_active?
|
|
50
52
|
return @editor_state.collapse_to_primary_selection if @editor_state.multi_cursor?
|
|
51
53
|
return @editor_state.clear_selection if @editor_state.selection_active?
|
|
52
54
|
when "/"
|
|
@@ -79,6 +81,10 @@ module Kward
|
|
|
79
81
|
def handle_modern_csi_u_key(key)
|
|
80
82
|
sequence = parse_csi_u_key(key)
|
|
81
83
|
return false unless sequence
|
|
84
|
+
if editor_search_active?
|
|
85
|
+
search_result = handle_editor_search_csi_u_key(sequence, enter: :repeat, restore_cursor_on_cancel: false)
|
|
86
|
+
return search_result unless search_result == false
|
|
87
|
+
end
|
|
82
88
|
|
|
83
89
|
code = sequence[:code]
|
|
84
90
|
modifier = sequence[:modifier]
|
|
@@ -197,7 +203,7 @@ module Kward
|
|
|
197
203
|
when TerminalKeys::CTRL_C
|
|
198
204
|
editor_search_active? ? editor_search_cancel : copy_editor_selection
|
|
199
205
|
when TerminalKeys::CTRL_F
|
|
200
|
-
|
|
206
|
+
editor_search_begin(:forward) unless editor_search_active?
|
|
201
207
|
when TerminalKeys::CTRL_V
|
|
202
208
|
modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
|
|
203
209
|
when TerminalKeys::CTRL_X
|
|
@@ -229,7 +235,7 @@ module Kward
|
|
|
229
235
|
|
|
230
236
|
@editor_state.add_next_occurrence_selection
|
|
231
237
|
when 102
|
|
232
|
-
|
|
238
|
+
editor_search_begin(:forward) unless editor_search_active?
|
|
233
239
|
when 118
|
|
234
240
|
modern_record_undo { @editor_state.yank_kill_buffer } unless editor_search_active?
|
|
235
241
|
when 120
|