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,524 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "set"
|
|
3
|
+
require_relative "../config_files"
|
|
4
|
+
require_relative "../private_file"
|
|
5
|
+
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
7
|
+
module Kward
|
|
8
|
+
# Nested project file browser overlay behavior.
|
|
9
|
+
class PromptInterface
|
|
10
|
+
# Modal tree browser for project files.
|
|
11
|
+
module ProjectBrowser
|
|
12
|
+
PROJECT_BROWSER_ROOT = "".freeze
|
|
13
|
+
PROJECT_BROWSER_RESULT_LIMIT = 200
|
|
14
|
+
PROJECT_BROWSER_STATE_VERSION = 1
|
|
15
|
+
|
|
16
|
+
def open_project_browser
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
open_project_browser_locked
|
|
19
|
+
render_prompt_locked if @started && @asking
|
|
20
|
+
end
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def open_project_browser_locked
|
|
25
|
+
paths = project_file_paths
|
|
26
|
+
saved_state = saved_project_browser_state
|
|
27
|
+
@project_browser_state = {
|
|
28
|
+
paths: paths,
|
|
29
|
+
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
30
|
+
selection_index: 0,
|
|
31
|
+
search_active: false,
|
|
32
|
+
query: ""
|
|
33
|
+
}
|
|
34
|
+
restore_project_browser_selection(saved_state["selected_path"])
|
|
35
|
+
self.composer_input = ""
|
|
36
|
+
self.composer_cursor = 0
|
|
37
|
+
@pending_keys.clear
|
|
38
|
+
@asking = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def project_browser_visible?
|
|
44
|
+
!@project_browser_state.nil? && !editor_active?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def dismiss_project_browser
|
|
48
|
+
return false unless project_browser_visible?
|
|
49
|
+
|
|
50
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
51
|
+
@project_browser_state = nil
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_project_browser_key(key)
|
|
56
|
+
return true if handle_bundled_key(key) { |token| handle_project_browser_key(token) }
|
|
57
|
+
|
|
58
|
+
csi_result = handle_project_browser_csi_u_key(key)
|
|
59
|
+
return csi_result unless csi_result == false
|
|
60
|
+
|
|
61
|
+
case key_name_for(key)
|
|
62
|
+
when :return, :enter
|
|
63
|
+
open_or_toggle_selected_project_browser_row
|
|
64
|
+
when :backspace
|
|
65
|
+
project_browser_delete_search_character
|
|
66
|
+
when :ctrl_l
|
|
67
|
+
redraw_screen_locked
|
|
68
|
+
when :left
|
|
69
|
+
collapse_selected_project_browser_row
|
|
70
|
+
when :right
|
|
71
|
+
expand_selected_project_browser_row
|
|
72
|
+
when :up
|
|
73
|
+
select_previous_project_browser_row
|
|
74
|
+
when :down
|
|
75
|
+
select_next_project_browser_row
|
|
76
|
+
else
|
|
77
|
+
handle_project_browser_raw_key(key)
|
|
78
|
+
end
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def handle_project_browser_csi_u_key(key)
|
|
83
|
+
sequence = parse_csi_u_key(key)
|
|
84
|
+
return false unless sequence
|
|
85
|
+
|
|
86
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
87
|
+
case sequence[:code]
|
|
88
|
+
when 13
|
|
89
|
+
open_or_toggle_selected_project_browser_row
|
|
90
|
+
when 27
|
|
91
|
+
project_browser_escape
|
|
92
|
+
when 8, 127
|
|
93
|
+
project_browser_delete_search_character
|
|
94
|
+
when 9
|
|
95
|
+
return false if ctrl_modifier?(sequence[:modifier]) || alt_modifier?(sequence[:modifier]) || super_modifier?(sequence[:modifier])
|
|
96
|
+
|
|
97
|
+
toggle_project_browser_search
|
|
98
|
+
else
|
|
99
|
+
text = csi_u_printable_text(sequence)
|
|
100
|
+
return false unless text
|
|
101
|
+
|
|
102
|
+
if text == "@"
|
|
103
|
+
insert_selected_project_browser_mention
|
|
104
|
+
elsif text == "/" && !project_browser_search_active?
|
|
105
|
+
activate_project_browser_search
|
|
106
|
+
elsif project_browser_search_active?
|
|
107
|
+
project_browser_append_search(text)
|
|
108
|
+
else
|
|
109
|
+
return false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_project_browser_raw_key(key)
|
|
116
|
+
case key
|
|
117
|
+
when "\n", "\r"
|
|
118
|
+
open_or_toggle_selected_project_browser_row
|
|
119
|
+
when "\b", "\x7F"
|
|
120
|
+
project_browser_delete_search_character
|
|
121
|
+
when "\e"
|
|
122
|
+
project_browser_escape
|
|
123
|
+
when "@"
|
|
124
|
+
insert_selected_project_browser_mention
|
|
125
|
+
when "\t"
|
|
126
|
+
toggle_project_browser_search
|
|
127
|
+
when "/"
|
|
128
|
+
activate_project_browser_search
|
|
129
|
+
else
|
|
130
|
+
project_browser_append_search(key) if project_browser_search_active? && printable_key?(key)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def project_browser_escape
|
|
135
|
+
if project_browser_search_active?
|
|
136
|
+
deactivate_project_browser_search
|
|
137
|
+
else
|
|
138
|
+
dismiss_project_browser
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def toggle_project_browser_search
|
|
143
|
+
project_browser_search_active? ? deactivate_project_browser_search : activate_project_browser_search
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def activate_project_browser_search
|
|
147
|
+
@project_browser_state[:search_active] = true
|
|
148
|
+
@project_browser_state[:query] = ""
|
|
149
|
+
@project_browser_state[:selection_index] = 0
|
|
150
|
+
sync_project_browser_query_input
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def deactivate_project_browser_search
|
|
154
|
+
@project_browser_state[:search_active] = false
|
|
155
|
+
@project_browser_state[:query] = ""
|
|
156
|
+
sync_project_browser_query_input
|
|
157
|
+
clamp_project_browser_selection
|
|
158
|
+
persist_project_browser_state
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def project_browser_append_search(key)
|
|
162
|
+
@project_browser_state[:query] += key
|
|
163
|
+
@project_browser_state[:selection_index] = 0
|
|
164
|
+
sync_project_browser_query_input
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def project_browser_delete_search_character
|
|
168
|
+
return unless project_browser_search_active?
|
|
169
|
+
return if @project_browser_state[:query].empty?
|
|
170
|
+
|
|
171
|
+
@project_browser_state[:query] = @project_browser_state[:query][0...-1]
|
|
172
|
+
sync_project_browser_query_input
|
|
173
|
+
clamp_project_browser_selection
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def sync_project_browser_query_input
|
|
177
|
+
self.composer_input = project_browser_search_active? ? @project_browser_state[:query].to_s : ""
|
|
178
|
+
self.composer_cursor = composer_input.length
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def project_browser_search_active?
|
|
182
|
+
@project_browser_state && @project_browser_state[:search_active]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def select_previous_project_browser_row
|
|
186
|
+
rows = project_browser_visible_rows
|
|
187
|
+
return if rows.empty?
|
|
188
|
+
|
|
189
|
+
@project_browser_state[:selection_index] = previous_list_selection_index(@project_browser_state[:selection_index], rows.length)
|
|
190
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def select_next_project_browser_row
|
|
194
|
+
rows = project_browser_visible_rows
|
|
195
|
+
return if rows.empty?
|
|
196
|
+
|
|
197
|
+
@project_browser_state[:selection_index] = next_list_selection_index(@project_browser_state[:selection_index], rows.length)
|
|
198
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def open_or_toggle_selected_project_browser_row
|
|
202
|
+
row = selected_project_browser_row
|
|
203
|
+
return false unless row
|
|
204
|
+
|
|
205
|
+
if row[:directory]
|
|
206
|
+
toggle_project_browser_directory(row[:path])
|
|
207
|
+
true
|
|
208
|
+
else
|
|
209
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
210
|
+
@project_browser_restore_after_editor = true if open_editor(row[:path])
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def expand_selected_project_browser_row
|
|
216
|
+
row = selected_project_browser_row
|
|
217
|
+
return false unless row&.fetch(:directory, false)
|
|
218
|
+
|
|
219
|
+
@project_browser_state[:expanded].add(row[:path])
|
|
220
|
+
persist_project_browser_state
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def collapse_selected_project_browser_row
|
|
225
|
+
row = selected_project_browser_row
|
|
226
|
+
return false unless row
|
|
227
|
+
|
|
228
|
+
if row[:directory] && @project_browser_state[:expanded].include?(row[:path])
|
|
229
|
+
@project_browser_state[:expanded].delete(row[:path]) unless row[:path] == PROJECT_BROWSER_ROOT
|
|
230
|
+
clamp_project_browser_selection
|
|
231
|
+
persist_project_browser_state
|
|
232
|
+
true
|
|
233
|
+
else
|
|
234
|
+
select_project_browser_parent(row[:path])
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def toggle_project_browser_directory(path)
|
|
239
|
+
expanded = @project_browser_state[:expanded]
|
|
240
|
+
if expanded.include?(path)
|
|
241
|
+
expanded.delete(path) unless path == PROJECT_BROWSER_ROOT
|
|
242
|
+
else
|
|
243
|
+
expanded.add(path)
|
|
244
|
+
end
|
|
245
|
+
clamp_project_browser_selection
|
|
246
|
+
persist_project_browser_state
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def select_project_browser_parent(path)
|
|
250
|
+
parent = File.dirname(path.to_s)
|
|
251
|
+
parent = PROJECT_BROWSER_ROOT if parent == "."
|
|
252
|
+
rows = project_browser_visible_rows
|
|
253
|
+
index = rows.index { |row| row[:directory] && row[:path] == parent }
|
|
254
|
+
return unless index
|
|
255
|
+
|
|
256
|
+
@project_browser_state[:selection_index] = index
|
|
257
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def insert_selected_project_browser_mention
|
|
261
|
+
row = selected_project_browser_row
|
|
262
|
+
return false unless row && !row[:directory]
|
|
263
|
+
|
|
264
|
+
persist_project_browser_state unless project_browser_search_active?
|
|
265
|
+
self.composer_input = "@#{row[:path]}"
|
|
266
|
+
self.composer_cursor = composer_input.length
|
|
267
|
+
dismiss_project_browser
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def restore_project_browser_after_editor_close
|
|
272
|
+
return unless @project_browser_restore_after_editor
|
|
273
|
+
|
|
274
|
+
@project_browser_restore_after_editor = false
|
|
275
|
+
unless @project_browser_state
|
|
276
|
+
paths = project_file_paths
|
|
277
|
+
saved_state = saved_project_browser_state
|
|
278
|
+
@project_browser_state = {
|
|
279
|
+
paths: paths,
|
|
280
|
+
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
281
|
+
selection_index: 0,
|
|
282
|
+
search_active: false,
|
|
283
|
+
query: ""
|
|
284
|
+
}
|
|
285
|
+
restore_project_browser_selection(saved_state["selected_path"])
|
|
286
|
+
end
|
|
287
|
+
sync_project_browser_query_input
|
|
288
|
+
clamp_project_browser_selection
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def project_browser_rows(width, height: screen_height)
|
|
292
|
+
return [] unless project_browser_visible?
|
|
293
|
+
|
|
294
|
+
rows = project_browser_visible_rows
|
|
295
|
+
lines = []
|
|
296
|
+
title = project_browser_title
|
|
297
|
+
if rows.empty?
|
|
298
|
+
lines << overlay_text_line(project_browser_empty_message, :muted)
|
|
299
|
+
else
|
|
300
|
+
visible = visible_project_browser_rows(rows, height: height)
|
|
301
|
+
visible[:rows].each_with_index do |row, offset|
|
|
302
|
+
index = visible[:start] + offset
|
|
303
|
+
lines << overlay_choice_line(project_browser_row_text(row), selected: index == @project_browser_state[:selection_index])
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
lines << overlay_blank_line
|
|
307
|
+
lines << overlay_text_line(project_browser_help_text, :muted)
|
|
308
|
+
overlay_card_rows(title, lines, width)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def project_browser_title
|
|
312
|
+
query = @project_browser_state[:query].to_s
|
|
313
|
+
project_browser_search_active? ? "Project files — Search: #{query}" : "Project files"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def project_browser_empty_message
|
|
317
|
+
project_browser_search_active? ? "No matching files" : "No project files"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def project_browser_help_text
|
|
321
|
+
if project_browser_search_active?
|
|
322
|
+
"Type search • Esc tree • Enter open • @ mention"
|
|
323
|
+
else
|
|
324
|
+
"Enter open/toggle • ←/→ collapse/expand • Tab or / search • @ mention • Esc close"
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def project_browser_visible_rows
|
|
329
|
+
return [] unless @project_browser_state
|
|
330
|
+
return project_browser_search_rows if project_browser_search_active?
|
|
331
|
+
|
|
332
|
+
tree = project_browser_tree
|
|
333
|
+
directory_children = tree[:directories].fetch(PROJECT_BROWSER_ROOT, [])
|
|
334
|
+
file_children = tree[:files].fetch(PROJECT_BROWSER_ROOT, [])
|
|
335
|
+
rows = []
|
|
336
|
+
append_project_browser_rows(rows, directory_children, file_children, tree, 0)
|
|
337
|
+
rows
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def append_project_browser_rows(rows, directories, files, tree, depth)
|
|
341
|
+
directories.each do |directory|
|
|
342
|
+
expanded = @project_browser_state[:expanded].include?(directory)
|
|
343
|
+
rows << { path: directory, name: File.basename(directory), depth: depth, directory: true, expanded: expanded }
|
|
344
|
+
next unless expanded
|
|
345
|
+
|
|
346
|
+
append_project_browser_rows(
|
|
347
|
+
rows,
|
|
348
|
+
tree[:directories].fetch(directory, []),
|
|
349
|
+
tree[:files].fetch(directory, []),
|
|
350
|
+
tree,
|
|
351
|
+
depth + 1
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
files.each do |file|
|
|
355
|
+
rows << { path: file, name: File.basename(file), depth: depth, directory: false }
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def project_browser_search_rows
|
|
360
|
+
query = @project_browser_state[:query].downcase
|
|
361
|
+
matches = []
|
|
362
|
+
project_file_path_entries.each do |entry|
|
|
363
|
+
next unless file_mention_match?(entry[:downcase], query)
|
|
364
|
+
|
|
365
|
+
matches << { path: entry[:path], name: entry[:path], depth: 0, directory: false }
|
|
366
|
+
break if matches.length >= PROJECT_BROWSER_RESULT_LIMIT
|
|
367
|
+
end
|
|
368
|
+
matches
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def project_browser_tree
|
|
372
|
+
paths = @project_browser_state[:paths]
|
|
373
|
+
return @project_browser_tree if @project_browser_tree_paths.equal?(paths) && @project_browser_tree
|
|
374
|
+
|
|
375
|
+
directories = Hash.new { |hash, key| hash[key] = Set.new }
|
|
376
|
+
files = Hash.new { |hash, key| hash[key] = [] }
|
|
377
|
+
paths.each do |path|
|
|
378
|
+
parts = path.split("/")
|
|
379
|
+
parent = PROJECT_BROWSER_ROOT
|
|
380
|
+
parts[0...-1].each do |part|
|
|
381
|
+
directory = parent.empty? ? part : "#{parent}/#{part}"
|
|
382
|
+
directories[parent].add(directory)
|
|
383
|
+
parent = directory
|
|
384
|
+
end
|
|
385
|
+
files[parent] << path
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
@project_browser_tree_paths = paths
|
|
389
|
+
@project_browser_tree = {
|
|
390
|
+
directories: directories.transform_values { |values| values.to_a.sort },
|
|
391
|
+
files: files.transform_values(&:sort)
|
|
392
|
+
}
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def project_browser_row_text(row)
|
|
396
|
+
indent = " " * row[:depth]
|
|
397
|
+
marker = if row[:directory]
|
|
398
|
+
row[:expanded] ? "▾ " : "▸ "
|
|
399
|
+
else
|
|
400
|
+
" "
|
|
401
|
+
end
|
|
402
|
+
suffix = row[:directory] ? "/" : ""
|
|
403
|
+
"#{indent}#{marker}#{row[:name]}#{suffix}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def saved_project_browser_state
|
|
407
|
+
workspaces = read_project_browser_state_file["workspaces"]
|
|
408
|
+
state = workspaces[project_browser_workspace_root] if workspaces.is_a?(Hash)
|
|
409
|
+
state.is_a?(Hash) ? state : {}
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def persist_project_browser_state
|
|
413
|
+
return unless @project_browser_state
|
|
414
|
+
|
|
415
|
+
data = read_project_browser_state_file
|
|
416
|
+
workspaces = data["workspaces"].is_a?(Hash) ? data["workspaces"] : {}
|
|
417
|
+
row = selected_project_browser_row
|
|
418
|
+
workspaces[project_browser_workspace_root] = {
|
|
419
|
+
"expanded" => @project_browser_state[:expanded].to_a.sort,
|
|
420
|
+
"selected_path" => row && row[:path]
|
|
421
|
+
}
|
|
422
|
+
data["version"] = PROJECT_BROWSER_STATE_VERSION
|
|
423
|
+
data["workspaces"] = workspaces
|
|
424
|
+
PrivateFile.write_json(ConfigFiles.project_browser_state_path, data)
|
|
425
|
+
rescue StandardError
|
|
426
|
+
nil
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def read_project_browser_state_file
|
|
430
|
+
path = ConfigFiles.project_browser_state_path
|
|
431
|
+
return { "version" => PROJECT_BROWSER_STATE_VERSION, "workspaces" => {} } unless File.exist?(path)
|
|
432
|
+
|
|
433
|
+
data = JSON.parse(File.read(path))
|
|
434
|
+
data.is_a?(Hash) ? data : { "version" => PROJECT_BROWSER_STATE_VERSION, "workspaces" => {} }
|
|
435
|
+
rescue JSON::ParserError
|
|
436
|
+
{ "version" => PROJECT_BROWSER_STATE_VERSION, "workspaces" => {} }
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def project_browser_workspace_root
|
|
440
|
+
ConfigFiles.canonical_workspace_root(Dir.pwd)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def restored_project_browser_expanded_paths(paths, saved_state)
|
|
444
|
+
directories = project_browser_directory_paths(paths)
|
|
445
|
+
saved_expanded = saved_state["expanded"]
|
|
446
|
+
expanded = if saved_expanded.is_a?(Array)
|
|
447
|
+
Set.new(saved_expanded.select { |path| directories.include?(path.to_s) })
|
|
448
|
+
else
|
|
449
|
+
default_project_browser_expanded_paths(paths)
|
|
450
|
+
end
|
|
451
|
+
expanded.add(PROJECT_BROWSER_ROOT)
|
|
452
|
+
expanded
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def project_browser_directory_paths(paths)
|
|
456
|
+
directories = Set.new([PROJECT_BROWSER_ROOT])
|
|
457
|
+
paths.each do |path|
|
|
458
|
+
parent = PROJECT_BROWSER_ROOT
|
|
459
|
+
path.split("/")[0...-1].each do |part|
|
|
460
|
+
parent = parent.empty? ? part : "#{parent}/#{part}"
|
|
461
|
+
directories.add(parent)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
directories
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def restore_project_browser_selection(path)
|
|
468
|
+
rows = project_browser_visible_rows
|
|
469
|
+
return @project_browser_state[:selection_index] = 0 if rows.empty?
|
|
470
|
+
|
|
471
|
+
index = project_browser_selection_fallback_paths(path).filter_map do |candidate|
|
|
472
|
+
rows.index { |row| row[:path] == candidate }
|
|
473
|
+
end.first
|
|
474
|
+
@project_browser_state[:selection_index] = index || 0
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def project_browser_selection_fallback_paths(path)
|
|
478
|
+
current = path.to_s
|
|
479
|
+
candidates = []
|
|
480
|
+
until current.empty? || current == "."
|
|
481
|
+
candidates << current
|
|
482
|
+
current = File.dirname(current)
|
|
483
|
+
end
|
|
484
|
+
candidates
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def selected_project_browser_row
|
|
488
|
+
rows = project_browser_visible_rows
|
|
489
|
+
return nil if rows.empty?
|
|
490
|
+
|
|
491
|
+
rows[[@project_browser_state[:selection_index], rows.length - 1].min]
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def clamp_project_browser_selection
|
|
495
|
+
rows = project_browser_visible_rows
|
|
496
|
+
@project_browser_state[:selection_index] = 0 if rows.empty?
|
|
497
|
+
@project_browser_state[:selection_index] = [[@project_browser_state[:selection_index], 0].max, rows.length - 1].min unless rows.empty?
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def visible_project_browser_rows(rows, height: screen_height)
|
|
501
|
+
max_rows = max_project_browser_rows(height)
|
|
502
|
+
start = centered_list_window_start(@project_browser_state[:selection_index], rows.length, max_rows)
|
|
503
|
+
{ start: start, rows: rows[start, max_rows] || [] }
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def max_project_browser_rows(height)
|
|
507
|
+
[[height - 8, 4].max, 20].min
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def default_project_browser_expanded_paths(paths)
|
|
511
|
+
expanded = Set.new([PROJECT_BROWSER_ROOT])
|
|
512
|
+
paths.each do |path|
|
|
513
|
+
parts = path.split("/")
|
|
514
|
+
parent = PROJECT_BROWSER_ROOT
|
|
515
|
+
parts[0...-1].first(2).each do |part|
|
|
516
|
+
parent = parent.empty? ? part : "#{parent}/#{part}"
|
|
517
|
+
expanded.add(parent)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
expanded
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|