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,321 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Endwise-style closing keyword insertion for the built-in editor.
|
|
6
|
+
module EditorEndwise
|
|
7
|
+
ENDWISE_LINE_PARSE_LIMIT = 100_000
|
|
8
|
+
ENDWISE_SINGLE_LINE_DEFINITION = /;\s*end[\s;]*\z/.freeze
|
|
9
|
+
ENDWISE_ENDLESS_DEFINITION = /\A\s*?def\s+[^\s(]+\s*(?:\(.*\))?\s*=/.freeze
|
|
10
|
+
|
|
11
|
+
ENDWISE_LANGUAGES = {
|
|
12
|
+
ruby: {
|
|
13
|
+
line_comments: ["#"],
|
|
14
|
+
block_comments: [{ start: /\A\s*=begin\b/, end: /\A\s*=end\b/ }],
|
|
15
|
+
close_pattern: /\Aend\b/,
|
|
16
|
+
openings: [
|
|
17
|
+
{ pattern: /\A\s*?if(\s|\()/, close: "end" },
|
|
18
|
+
{ pattern: /\A\s*?unless(\s|\()/, close: "end" },
|
|
19
|
+
{ pattern: /\A\s*?while(\s|\()/, close: "end" },
|
|
20
|
+
{ pattern: /\A\s*?for(\s|\()/, close: "end" },
|
|
21
|
+
{ pattern: /\s?do(\s?\z|\s\|.*\|\s?\z)/, close: "end" },
|
|
22
|
+
{ pattern: /\A\s*?def\s/, close: "end" },
|
|
23
|
+
{ pattern: /\A\s*?class\s/, close: "end" },
|
|
24
|
+
{ pattern: /\A\s*?module\s/, close: "end" },
|
|
25
|
+
{ pattern: /\A\s*?case(\s|\()/, close: "end" },
|
|
26
|
+
{ pattern: /\A\s*?begin\s/, close: "end" },
|
|
27
|
+
{ pattern: /\A\s*?until(\s|\()/, close: "end" }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
crystal: {
|
|
31
|
+
line_comments: ["#"],
|
|
32
|
+
block_comments: [],
|
|
33
|
+
close_pattern: /\Aend\b/,
|
|
34
|
+
openings: [
|
|
35
|
+
{ pattern: /\A\s*?if(\s|\()/, close: "end" },
|
|
36
|
+
{ pattern: /\A\s*?unless(\s|\()/, close: "end" },
|
|
37
|
+
{ pattern: /\A\s*?while(\s|\()/, close: "end" },
|
|
38
|
+
{ pattern: /\A\s*?for(\s|\()/, close: "end" },
|
|
39
|
+
{ pattern: /\s?do(\s?\z|\s\|.*\|\s?\z)/, close: "end" },
|
|
40
|
+
{ pattern: /\A\s*?enum\s/, close: "end" },
|
|
41
|
+
{ pattern: /\A\s*?struct\s/, close: "end" },
|
|
42
|
+
{ pattern: /\A\s*?macro\s/, close: "end" },
|
|
43
|
+
{ pattern: /\A\s*?union\s/, close: "end" },
|
|
44
|
+
{ pattern: /\A\s*?lib\s/, close: "end" },
|
|
45
|
+
{ pattern: /\A\s*?annotation\s/, close: "end" },
|
|
46
|
+
{ pattern: /\A\s*?def\s/, close: "end" },
|
|
47
|
+
{ pattern: /\A\s*?class\s/, close: "end" },
|
|
48
|
+
{ pattern: /\A\s*?module\s/, close: "end" },
|
|
49
|
+
{ pattern: /\A\s*?case(\s|\()/, close: "end" },
|
|
50
|
+
{ pattern: /\A\s*?begin\s/, close: "end" },
|
|
51
|
+
{ pattern: /\A\s*?until(\s|\()/, close: "end" }
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
elixir: {
|
|
55
|
+
line_comments: ["#"],
|
|
56
|
+
block_comments: [],
|
|
57
|
+
close_pattern: /\Aend\b/,
|
|
58
|
+
openings: [
|
|
59
|
+
{ pattern: /\bdo\s*\z/, close: "end" },
|
|
60
|
+
{ pattern: /\A\s*fn\s*\z/, close: "end" },
|
|
61
|
+
{ pattern: /\bfn\b.*->\s*\z/, close: "end" }
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
julia: {
|
|
65
|
+
line_comments: ["#"],
|
|
66
|
+
block_comments: [],
|
|
67
|
+
close_pattern: /\Aend\b/,
|
|
68
|
+
openings: [
|
|
69
|
+
{ pattern: /\A\s*begin\s*\z/, close: "end" },
|
|
70
|
+
{ pattern: /\A\s*if\b/, close: "end" },
|
|
71
|
+
{ pattern: /\A\s*while\b/, close: "end" },
|
|
72
|
+
{ pattern: /\A\s*for\b/, close: "end" },
|
|
73
|
+
{ pattern: /\A\s*try\s*\z/, close: "end" },
|
|
74
|
+
{ pattern: /\A\s*let(?:\s|\z)/, close: "end" },
|
|
75
|
+
{ pattern: /\A\s*quote\s*\z/, close: "end" },
|
|
76
|
+
{ pattern: /\A\s*function\b/, close: "end" },
|
|
77
|
+
{ pattern: /\A\s*macro\b/, close: "end" },
|
|
78
|
+
{ pattern: /\A\s*module\b/, close: "end" },
|
|
79
|
+
{ pattern: /\A\s*baremodule\b/, close: "end" },
|
|
80
|
+
{ pattern: /\A\s*(?:mutable\s+)?struct\b/, close: "end" },
|
|
81
|
+
{ pattern: /\A\s*abstract\s+type\b/, close: "end" },
|
|
82
|
+
{ pattern: /\A\s*primitive\s+type\b/, close: "end" },
|
|
83
|
+
{ pattern: /\bdo(?:\s+.*)?\s*\z/, close: "end" }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
lua: {
|
|
87
|
+
line_comments: ["--"],
|
|
88
|
+
block_comments: [{ start: /\A\s*--\[\[/, end: /\]\]/ }],
|
|
89
|
+
close_pattern: /\Aend\b/,
|
|
90
|
+
openings: [
|
|
91
|
+
{ pattern: /\A\s*do\s*\z/, close: "end" },
|
|
92
|
+
{ pattern: /\A\s*while\b.*\bdo\s*\z/, close: "end" },
|
|
93
|
+
{ pattern: /\A\s*if\b.*\bthen\s*\z/, close: "end" },
|
|
94
|
+
{ pattern: /\A\s*for\b.*\bdo\s*\z/, close: "end" },
|
|
95
|
+
{ pattern: /\A\s*(?:local\s+)?function\b.*\)\s*\z/, close: "end" }
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
makefile: {
|
|
99
|
+
line_comments: ["#"],
|
|
100
|
+
block_comments: [],
|
|
101
|
+
close_pattern: /\Aendif\b/,
|
|
102
|
+
openings: [
|
|
103
|
+
{ pattern: /\A\s*if(?:eq|neq)\b/, close: "endif" },
|
|
104
|
+
{ pattern: /\A\s*ifn?def\b/, close: "endif" }
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
shell: {
|
|
108
|
+
line_comments: ["#"],
|
|
109
|
+
block_comments: [],
|
|
110
|
+
close_pattern: /\A(?:fi|done|esac)\b/,
|
|
111
|
+
openings: [
|
|
112
|
+
{ pattern: /\bthen\s*\z/, close: "fi" },
|
|
113
|
+
{ pattern: /\A\s*case\b/, close: "esac" },
|
|
114
|
+
{ pattern: /\bdo\s*\z/, close: "done" }
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
}.freeze
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def editor_insert_endwise_newline
|
|
122
|
+
plan = editor_endwise_plan(called_with_modifier: false)
|
|
123
|
+
return false unless plan
|
|
124
|
+
|
|
125
|
+
editor_apply_endwise_plan(plan)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def editor_insert_endwise_modifier_newline
|
|
129
|
+
plan = editor_endwise_plan(called_with_modifier: true) || editor_endwise_plain_modifier_plan
|
|
130
|
+
editor_apply_endwise_plan(plan)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def editor_endwise_plan(called_with_modifier:)
|
|
134
|
+
return nil unless current_editor_auto_indent?
|
|
135
|
+
|
|
136
|
+
line_index, column = @editor_state.cursor_line_and_column
|
|
137
|
+
return nil unless editor_endwise_line_exists?(line_index)
|
|
138
|
+
|
|
139
|
+
language = editor_syntax_language
|
|
140
|
+
definition = ENDWISE_LANGUAGES[language]
|
|
141
|
+
return nil unless definition
|
|
142
|
+
|
|
143
|
+
line = @editor_state.lines[line_index].to_s
|
|
144
|
+
line_length = line.length
|
|
145
|
+
return nil if !called_with_modifier && line_length > column
|
|
146
|
+
|
|
147
|
+
closing_indent = editor_endwise_indentation_for(line)
|
|
148
|
+
inner_indent = closing_indent + editor_indent_unit
|
|
149
|
+
close = editor_endwise_closing_keyword_for_line(line_index, column, called_with_modifier: called_with_modifier)
|
|
150
|
+
target_offset = @editor_state.line_start_offset(line_index) + line_length
|
|
151
|
+
|
|
152
|
+
if close
|
|
153
|
+
return {
|
|
154
|
+
cursor_column: inner_indent.length,
|
|
155
|
+
cursor_line: line_index + 1,
|
|
156
|
+
offset: target_offset,
|
|
157
|
+
text: "\n#{inner_indent}\n#{closing_indent}#{close}"
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return nil unless called_with_modifier && editor_endwise_line_opens_block?(line_index, language)
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
cursor_column: inner_indent.length,
|
|
165
|
+
cursor_line: line_index + 1,
|
|
166
|
+
offset: target_offset,
|
|
167
|
+
text: "\n#{inner_indent}"
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def editor_endwise_plain_modifier_plan
|
|
172
|
+
line_index, = @editor_state.cursor_line_and_column
|
|
173
|
+
line = @editor_state.lines[line_index].to_s
|
|
174
|
+
{
|
|
175
|
+
cursor_column: 0,
|
|
176
|
+
cursor_line: line_index + 1,
|
|
177
|
+
offset: @editor_state.line_start_offset(line_index) + line.length,
|
|
178
|
+
text: "\n"
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def editor_apply_endwise_plan(plan)
|
|
183
|
+
@editor_state.cursor = plan.fetch(:offset)
|
|
184
|
+
@editor_state.insert(plan.fetch(:text))
|
|
185
|
+
@editor_state.set_cursor_line_and_column(plan.fetch(:cursor_line), plan.fetch(:cursor_column))
|
|
186
|
+
true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def editor_endwise_line_exists?(line_index)
|
|
190
|
+
line_index >= 0 && line_index < @editor_state.lines.length
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def editor_endwise_line_opens_block?(line_index, language = editor_syntax_language)
|
|
194
|
+
code = editor_endwise_code_line_at(line_index, language)
|
|
195
|
+
return false if editor_endwise_ignored_definition?(code, language)
|
|
196
|
+
|
|
197
|
+
ENDWISE_LANGUAGES.fetch(language).fetch(:openings).any? do |opening|
|
|
198
|
+
code.match?(opening.fetch(:pattern))
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def editor_endwise_closing_keyword_for_line(line_index, column, called_with_modifier: false)
|
|
203
|
+
language = editor_syntax_language
|
|
204
|
+
definition = ENDWISE_LANGUAGES[language]
|
|
205
|
+
return nil unless definition
|
|
206
|
+
|
|
207
|
+
openings = definition.fetch(:openings)
|
|
208
|
+
line = @editor_state.lines[line_index].to_s
|
|
209
|
+
code = editor_endwise_code_line_at(line_index, language)
|
|
210
|
+
current_indent = editor_endwise_indentation_for(line)
|
|
211
|
+
|
|
212
|
+
return nil if !called_with_modifier && line.length > column
|
|
213
|
+
return nil if editor_endwise_ignored_definition?(code, language)
|
|
214
|
+
|
|
215
|
+
openings.each do |opening|
|
|
216
|
+
next unless code.match?(opening.fetch(:pattern))
|
|
217
|
+
|
|
218
|
+
close = opening.fetch(:close)
|
|
219
|
+
stack_count = 0
|
|
220
|
+
(line_index..(line_index + ENDWISE_LINE_PARSE_LIMIT)).each do |scan_line|
|
|
221
|
+
return close if @editor_state.lines.length <= scan_line + 1
|
|
222
|
+
|
|
223
|
+
line_below = @editor_state.lines[scan_line + 1].to_s
|
|
224
|
+
code_below = editor_endwise_code_line_at(scan_line + 1, language)
|
|
225
|
+
closes_any_block = editor_endwise_closes_block?(code_below, language)
|
|
226
|
+
closes_this_block = editor_endwise_closes_with?(code_below, close)
|
|
227
|
+
|
|
228
|
+
if current_indent.length > editor_endwise_indentation_for(line_below).length && closes_any_block
|
|
229
|
+
return close
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
next unless current_indent == editor_endwise_indentation_for(line_below)
|
|
233
|
+
|
|
234
|
+
if openings.any? { |inner_opening| code_below.match?(inner_opening.fetch(:pattern)) }
|
|
235
|
+
stack_count += 1
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
if closes_any_block && stack_count.positive?
|
|
239
|
+
stack_count -= 1
|
|
240
|
+
elsif closes_this_block
|
|
241
|
+
return nil
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def editor_endwise_ignored_definition?(code, language)
|
|
250
|
+
return true if code.match?(ENDWISE_SINGLE_LINE_DEFINITION)
|
|
251
|
+
return true if %i[ruby crystal].include?(language) && code.match?(ENDWISE_ENDLESS_DEFINITION)
|
|
252
|
+
|
|
253
|
+
false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def editor_endwise_code_line_at(line_index, language)
|
|
257
|
+
definition = ENDWISE_LANGUAGES[language]
|
|
258
|
+
line = @editor_state.lines[line_index].to_s
|
|
259
|
+
return "" if editor_endwise_inside_block_comment?(line_index, definition.fetch(:block_comments))
|
|
260
|
+
|
|
261
|
+
editor_endwise_strip_line_comment(line, definition.fetch(:line_comments))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def editor_endwise_inside_block_comment?(line_index, block_comments)
|
|
265
|
+
active_comment = nil
|
|
266
|
+
@editor_state.lines.first(line_index + 1).each_with_index do |line, index|
|
|
267
|
+
if active_comment
|
|
268
|
+
target_line = index == line_index
|
|
269
|
+
active_comment = nil if line.match?(active_comment.fetch(:end))
|
|
270
|
+
return true if target_line
|
|
271
|
+
|
|
272
|
+
next
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
block_comments.each do |block_comment|
|
|
276
|
+
start_match = line.match(block_comment.fetch(:start))
|
|
277
|
+
next unless start_match
|
|
278
|
+
|
|
279
|
+
rest = line[(start_match.begin(0) + start_match[0].length)..].to_s
|
|
280
|
+
if rest.match?(block_comment.fetch(:end))
|
|
281
|
+
return true if index == line_index
|
|
282
|
+
|
|
283
|
+
next
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
active_comment = block_comment
|
|
287
|
+
return true if index == line_index
|
|
288
|
+
|
|
289
|
+
break
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
false
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def editor_endwise_strip_line_comment(line, line_comments)
|
|
297
|
+
comment_index = line_comments.filter_map do |comment|
|
|
298
|
+
index = line.index(comment)
|
|
299
|
+
index unless index.nil?
|
|
300
|
+
end.min
|
|
301
|
+
|
|
302
|
+
comment_index ? line[0...comment_index].to_s : line
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def editor_endwise_indentation_for(line)
|
|
306
|
+
trimmed = line.to_s.strip
|
|
307
|
+
return line.to_s if trimmed.empty?
|
|
308
|
+
|
|
309
|
+
line.to_s[0...line.to_s.index(trimmed)].to_s
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def editor_endwise_closes_block?(code, language)
|
|
313
|
+
code.to_s.strip.match?(ENDWISE_LANGUAGES.fetch(language).fetch(:close_pattern))
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def editor_endwise_closes_with?(code, close)
|
|
317
|
+
code.to_s.strip.match?(/\A#{Regexp.escape(close)}\b/)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
6
|
+
class PromptInterface
|
|
7
|
+
# Tracks the on-disk identity and original content for an editor buffer.
|
|
8
|
+
class EditorFileMarker
|
|
9
|
+
attr_reader :content, :digest, :mtime, :size
|
|
10
|
+
|
|
11
|
+
def initialize(path:, content:, new_file: false)
|
|
12
|
+
@path = path.to_s
|
|
13
|
+
@content = content.to_s
|
|
14
|
+
@digest = Digest::SHA256.hexdigest(@content)
|
|
15
|
+
refresh unless new_file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def refresh(content = @content)
|
|
19
|
+
@content = content.to_s
|
|
20
|
+
@digest = Digest::SHA256.hexdigest(@content)
|
|
21
|
+
stat = File.stat(@path)
|
|
22
|
+
@mtime = stat.mtime
|
|
23
|
+
@size = stat.size
|
|
24
|
+
rescue StandardError
|
|
25
|
+
@mtime = nil
|
|
26
|
+
@size = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def changed_on_disk?(new_file: false)
|
|
30
|
+
return false if new_file && !File.exist?(@path)
|
|
31
|
+
return true if new_file && File.exist?(@path)
|
|
32
|
+
return true unless File.exist?(@path)
|
|
33
|
+
|
|
34
|
+
File.read(@path) != @content
|
|
35
|
+
rescue StandardError
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Indentation-based navigation over editor lines.
|
|
6
|
+
class EditorIndentNavigation
|
|
7
|
+
def initialize(lines)
|
|
8
|
+
@lines = lines
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def indentation_level_for_line(line_index)
|
|
12
|
+
@lines[line_index].to_s.index(/\S/) || 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def empty_line?(line_index)
|
|
16
|
+
@lines[line_index].to_s.strip.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next_line(current_line, current_indentation)
|
|
20
|
+
end_line = @lines.length - 1
|
|
21
|
+
return nil if current_line == end_line
|
|
22
|
+
|
|
23
|
+
next_line = current_line + 1
|
|
24
|
+
jumping_over_space = indentation_level_for_line(next_line) != current_indentation || empty_line?(next_line)
|
|
25
|
+
|
|
26
|
+
(next_line..end_line).each do |line_index|
|
|
27
|
+
indentation = indentation_level_for_line(line_index)
|
|
28
|
+
if jumping_over_space && indentation == current_indentation && !empty_line?(line_index)
|
|
29
|
+
return line_index
|
|
30
|
+
elsif !jumping_over_space && (indentation != current_indentation || empty_line?(line_index))
|
|
31
|
+
return line_index - 1
|
|
32
|
+
elsif !jumping_over_space && indentation == current_indentation && line_index == end_line
|
|
33
|
+
return line_index
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def previous_line(current_line, current_indentation)
|
|
41
|
+
return nil if current_line.zero?
|
|
42
|
+
|
|
43
|
+
previous_line = current_line - 1
|
|
44
|
+
jumping_over_space = indentation_level_for_line(previous_line) != current_indentation || empty_line?(previous_line)
|
|
45
|
+
|
|
46
|
+
previous_line.downto(0) do |line_index|
|
|
47
|
+
indentation = indentation_level_for_line(line_index)
|
|
48
|
+
if jumping_over_space && indentation == current_indentation && !empty_line?(line_index)
|
|
49
|
+
return line_index
|
|
50
|
+
elsif !jumping_over_space && (indentation != current_indentation || empty_line?(line_index))
|
|
51
|
+
return line_index + 1
|
|
52
|
+
elsif !jumping_over_space && indentation == current_indentation && line_index.zero?
|
|
53
|
+
return line_index
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Kill buffer, kill ring, and yank-pop bookkeeping for editor buffers.
|
|
6
|
+
class EditorKillRing
|
|
7
|
+
attr_reader :kill_buffer, :kill_ring, :last_yank_range, :last_yank_index
|
|
8
|
+
|
|
9
|
+
def initialize(kill_buffer: "", kill_ring: [], last_yank_range: nil, last_yank_index: nil)
|
|
10
|
+
@kill_buffer = kill_buffer.to_s
|
|
11
|
+
@kill_ring = kill_ring
|
|
12
|
+
@last_yank_range = last_yank_range
|
|
13
|
+
@last_yank_index = last_yank_index
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def kill_buffer=(text)
|
|
17
|
+
@kill_buffer = text.to_s
|
|
18
|
+
clear_last_yank
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def kill_ring=(values)
|
|
22
|
+
@kill_ring = values.to_a
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def last_yank_range=(value)
|
|
26
|
+
@last_yank_range = value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def last_yank_index=(value)
|
|
30
|
+
@last_yank_index = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def push(text)
|
|
34
|
+
text = text.to_s
|
|
35
|
+
return false if text.empty?
|
|
36
|
+
|
|
37
|
+
@kill_buffer = text
|
|
38
|
+
@kill_ring.unshift(text)
|
|
39
|
+
@kill_ring.uniq!
|
|
40
|
+
@kill_ring = @kill_ring.first(30)
|
|
41
|
+
clear_last_yank
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def first_yank
|
|
46
|
+
text = @kill_ring.first.to_s
|
|
47
|
+
return nil if text.empty?
|
|
48
|
+
|
|
49
|
+
text
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def record_yank(start_index, end_index)
|
|
53
|
+
@last_yank_range = [start_index, end_index]
|
|
54
|
+
@last_yank_index = 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def next_yank_pop
|
|
58
|
+
return nil unless @last_yank_range && @last_yank_index
|
|
59
|
+
return nil if @kill_ring.length < 2
|
|
60
|
+
|
|
61
|
+
@last_yank_index = (@last_yank_index + 1) % @kill_ring.length
|
|
62
|
+
{
|
|
63
|
+
text: @kill_ring[@last_yank_index],
|
|
64
|
+
range: @last_yank_range
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def record_yank_pop(start_index, end_index)
|
|
69
|
+
@last_yank_range = [start_index, end_index]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clear_last_yank
|
|
73
|
+
@last_yank_range = nil
|
|
74
|
+
@last_yank_index = nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|