kward 0.79.0 → 0.80.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/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +58 -1
- data/Gemfile.lock +2 -2
- data/README.md +31 -38
- data/doc/agent-tools.md +9 -10
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +69 -26
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +7 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +2 -2
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +2 -2
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +13 -4
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +10 -2
- data/doc/rpc.md +54 -31
- data/doc/sandboxing.md +22 -0
- data/doc/security.md +22 -7
- data/doc/session-management.md +2 -4
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +2 -2
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +7 -0
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +4 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/tabs.rb +102 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +20 -0
- data/lib/kward/config_files.rb +26 -3
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +225 -20
- data/lib/kward/model/model_info.rb +49 -0
- data/lib/kward/model/payloads.rb +63 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +83 -13
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +80 -7
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +7 -7
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +53 -12
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +6 -5
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +60 -10
- data/lib/kward/rpc/session_manager.rb +109 -30
- data/lib/kward/session_store.rb +84 -24
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +7 -2
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace_factory.rb +19 -2
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/setup.rb +4 -0
- metadata +26 -1
|
@@ -47,20 +47,22 @@ module Kward
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def line_and_column_for(offset)
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
offset = [[offset.to_i, 0].max, @text.length].min
|
|
51
|
+
next_line = line_start_offsets.bsearch_index { |line_offset| line_offset > offset }
|
|
52
|
+
line_index = next_line ? [next_line - 1, 0].max : lines.length - 1
|
|
53
|
+
[line_index, [offset - line_start_offsets[line_index], lines[line_index].length].min]
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def offset_for_line_and_column(line_index, column)
|
|
55
57
|
values = lines
|
|
56
58
|
line_index = [[line_index.to_i, 0].max, values.length - 1].min
|
|
57
59
|
column = [[column.to_i, 0].max, values[line_index].length].min
|
|
58
|
-
|
|
60
|
+
line_start_offsets[line_index] + column
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
def line_start_offset(line_index)
|
|
62
64
|
line_index = [[line_index.to_i, 0].max, lines.length - 1].min
|
|
63
|
-
|
|
65
|
+
line_start_offsets[line_index]
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def line_range(line_index)
|
|
@@ -101,8 +103,20 @@ module Kward
|
|
|
101
103
|
|
|
102
104
|
private
|
|
103
105
|
|
|
106
|
+
def line_start_offsets
|
|
107
|
+
@line_start_offsets ||= begin
|
|
108
|
+
values = lines
|
|
109
|
+
offsets = [0]
|
|
110
|
+
(values.length - 1).times do |index|
|
|
111
|
+
offsets << offsets.last + values[index].length + 1
|
|
112
|
+
end
|
|
113
|
+
offsets
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
def invalidate_lines_cache
|
|
105
118
|
@lines_cache = nil
|
|
119
|
+
@line_start_offsets = nil
|
|
106
120
|
end
|
|
107
121
|
end
|
|
108
122
|
end
|
|
@@ -39,8 +39,8 @@ module Kward
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def history_file_open_command(path)
|
|
42
|
-
full_path = File.expand_path(path.to_s,
|
|
43
|
-
relative_path = Pathname.new(full_path).relative_path_from(Pathname.new(
|
|
42
|
+
full_path = File.expand_path(path.to_s, prompt_workspace_root)
|
|
43
|
+
relative_path = Pathname.new(full_path).relative_path_from(Pathname.new(prompt_workspace_root)).to_s
|
|
44
44
|
"$#{relative_path}"
|
|
45
45
|
rescue ArgumentError
|
|
46
46
|
"$#{path}"
|
|
@@ -71,9 +71,9 @@ module Kward
|
|
|
71
71
|
true
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def open_editor(path, allow_new: false, base_dir:
|
|
75
|
-
full_path = File.expand_path(path.to_s, base_dir)
|
|
76
|
-
root =
|
|
74
|
+
def open_editor(path, allow_new: false, base_dir: nil, restrict_to_workspace: true)
|
|
75
|
+
full_path = File.expand_path(path.to_s, base_dir || prompt_workspace_root)
|
|
76
|
+
root = prompt_workspace_root
|
|
77
77
|
if restrict_to_workspace && !(full_path == root || full_path.start_with?("#{root}/"))
|
|
78
78
|
@file_editor_open_status = "Cannot edit file outside workspace"
|
|
79
79
|
return false
|
|
@@ -1301,8 +1301,8 @@ module Kward
|
|
|
1301
1301
|
end
|
|
1302
1302
|
|
|
1303
1303
|
def bind_editor_save_path(path)
|
|
1304
|
-
full_path = File.expand_path(path.to_s.strip,
|
|
1305
|
-
root =
|
|
1304
|
+
full_path = File.expand_path(path.to_s.strip, prompt_workspace_root)
|
|
1305
|
+
root = prompt_workspace_root
|
|
1306
1306
|
unless full_path == root || full_path.start_with?("#{root}/")
|
|
1307
1307
|
@editor_state.status = "Cannot save outside workspace"
|
|
1308
1308
|
return false
|
|
@@ -280,8 +280,8 @@ module Kward
|
|
|
280
280
|
|
|
281
281
|
def vibe_path_completion_candidates(prefix)
|
|
282
282
|
directory_prefix, basename_prefix = vibe_split_path_completion_prefix(prefix)
|
|
283
|
-
search_directory = File.expand_path(directory_prefix.empty? ? "." : directory_prefix,
|
|
284
|
-
root =
|
|
283
|
+
search_directory = File.expand_path(directory_prefix.empty? ? "." : directory_prefix, prompt_workspace_root)
|
|
284
|
+
root = prompt_workspace_root
|
|
285
285
|
return [] unless search_directory == root || search_directory.start_with?("#{root}/")
|
|
286
286
|
return [] unless File.directory?(search_directory)
|
|
287
287
|
|
|
@@ -34,7 +34,7 @@ module Kward
|
|
|
34
34
|
rows = [editor_top_border(width)]
|
|
35
35
|
rows.concat(visible_lines.each_with_index.map do |line, index|
|
|
36
36
|
gutter = if index < actual_visible_count
|
|
37
|
-
editor_line_number_gutter(@editor_state.viewport_row + index)
|
|
37
|
+
editor_line_number_gutter(@editor_state.viewport_row + index, line_index)
|
|
38
38
|
else
|
|
39
39
|
editor_blank_line_number_gutter
|
|
40
40
|
end
|
|
@@ -59,7 +59,7 @@ module Kward
|
|
|
59
59
|
rows = [editor_top_border(width)]
|
|
60
60
|
rows.concat(visible_rows.map do |visual_row|
|
|
61
61
|
if visual_row
|
|
62
|
-
gutter = visual_row[:continuation] ? editor_blank_line_number_gutter : editor_line_number_gutter(visual_row[:line_index])
|
|
62
|
+
gutter = visual_row[:continuation] ? editor_blank_line_number_gutter : editor_line_number_gutter(visual_row[:line_index], line_index)
|
|
63
63
|
rendered_line = editor_render_line(visual_row[:line], visual_row[:line_index], text_width, column_offset: visual_row[:column_offset])
|
|
64
64
|
box_content_row(gutter + rendered_line, content_width)
|
|
65
65
|
else
|
|
@@ -230,16 +230,16 @@ module Kward
|
|
|
230
230
|
(column.to_i / text_width) * text_width
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
def editor_line_number_gutter(line_index)
|
|
234
|
-
number = editor_display_line_number(line_index).to_s.rjust(editor_line_number_gutter_width - 3)
|
|
235
|
-
|
|
233
|
+
def editor_line_number_gutter(line_index, cursor_line)
|
|
234
|
+
number = editor_display_line_number(line_index, cursor_line).to_s.rjust(editor_line_number_gutter_width - 3)
|
|
235
|
+
number_color = line_index == cursor_line ? :white : :dark_forest_green
|
|
236
|
+
colored(number, number_color) + colored(" │ ", :dark_forest_green)
|
|
236
237
|
end
|
|
237
238
|
|
|
238
|
-
def editor_display_line_number(line_index)
|
|
239
|
+
def editor_display_line_number(line_index, cursor_line)
|
|
239
240
|
return line_index + 1 unless current_editor_line_numbers == "relative"
|
|
240
241
|
return line_index + 1 if @editor_state.readonly?
|
|
241
242
|
|
|
242
|
-
cursor_line, = @editor_state.cursor_line_and_column
|
|
243
243
|
line_index == cursor_line ? line_index + 1 : (line_index - cursor_line).abs
|
|
244
244
|
end
|
|
245
245
|
|
|
@@ -257,7 +257,7 @@ module Kward
|
|
|
257
257
|
|
|
258
258
|
def editor_display_path
|
|
259
259
|
path = @editor_state.path || @editor_state.display_path
|
|
260
|
-
Pathname.new(path).relative_path_from(Pathname.new(
|
|
260
|
+
Pathname.new(path).relative_path_from(Pathname.new(prompt_workspace_root)).to_s
|
|
261
261
|
rescue StandardError
|
|
262
262
|
@editor_state.display_path || @editor_state.path
|
|
263
263
|
end
|
|
@@ -793,21 +793,15 @@ module Kward
|
|
|
793
793
|
end
|
|
794
794
|
|
|
795
795
|
def cursor_line_and_column_for(offset)
|
|
796
|
-
|
|
797
|
-
[before_cursor.count("\n"), (before_cursor.split("\n", -1).last || "").length]
|
|
796
|
+
@text_buffer.line_and_column_for(offset)
|
|
798
797
|
end
|
|
799
798
|
|
|
800
799
|
def line_start_offset(line_index)
|
|
801
|
-
|
|
802
|
-
line_index = [[line_index.to_i, 0].max, values.length - 1].min
|
|
803
|
-
values.first(line_index).sum { |line| line.length + 1 }
|
|
800
|
+
@text_buffer.line_start_offset(line_index)
|
|
804
801
|
end
|
|
805
802
|
|
|
806
803
|
def line_range(line_index)
|
|
807
|
-
|
|
808
|
-
end_index = start_index + lines[line_index].to_s.length
|
|
809
|
-
end_index += 1 if end_index < @buffer.length
|
|
810
|
-
[start_index, end_index]
|
|
804
|
+
@text_buffer.line_range(line_index)
|
|
811
805
|
end
|
|
812
806
|
|
|
813
807
|
def word_range_at(offset)
|
|
@@ -11,12 +11,14 @@ module Kward
|
|
|
11
11
|
].freeze
|
|
12
12
|
RUBY_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|:[a-zA-Z_]\w*[!?=]?|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(RUBY_KEYWORDS)})\b)/.freeze
|
|
13
13
|
MARKDOWN_PATTERN = /(`[^`\n]+`|!?\[[^\]\n]+\]\([^\)\n]+\)|(?:\*\*|__)[^\n]+?(?:\*\*|__)|(?:\*|_)[^\n]+?(?:\*|_))/.freeze
|
|
14
|
-
|
|
14
|
+
HTML_TAG_START_PATTERN = /<\/?[A-Za-z][\w:-]*/.freeze
|
|
15
15
|
CSS_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|#[0-9a-fA-F]{3,8}\b|\.[A-Za-z_-][\w-]*|#[A-Za-z_-][\w-]*|\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms)?\b|[A-Za-z_-][\w-]*(?=\s*:)|@[A-Za-z_-][\w-]*)/.freeze
|
|
16
16
|
JSON_PATTERN = /("(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/.freeze
|
|
17
17
|
YAML_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:true|false|null|yes|no|on|off)\b|\b\d+(?:\.\d+)?\b|[A-Za-z0-9_-]+(?=\s*:))/.freeze
|
|
18
18
|
SQL_PATTERN = /("(?:\\.|[^"\\])*"|'(?:''|[^'])*'|--.*|\b\d+(?:\.\d+)?\b|\b(?:SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|INSERT|INTO|UPDATE|DELETE|CREATE|ALTER|DROP|TABLE|VIEW|INDEX|VALUES|SET|AND|OR|NOT|NULL|IS|AS|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|DISTINCT|UNION|ALL|CASE|WHEN|THEN|ELSE|END|PRIMARY|KEY|FOREIGN|REFERENCES|DEFAULT|TRUE|FALSE)\b)/i.freeze
|
|
19
19
|
GENERIC_STRING_NUMBER_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b)/.freeze
|
|
20
|
+
ERB_OPEN_PATTERN = /<%[#=]?/.freeze
|
|
21
|
+
ERB_CLOSE_PATTERN = /-?%>/.freeze
|
|
20
22
|
|
|
21
23
|
LANGUAGE_DEFINITIONS = {
|
|
22
24
|
javascript: {
|
|
@@ -103,6 +105,7 @@ module Kward
|
|
|
103
105
|
JSON_EXTENSIONS = %w[.json].freeze
|
|
104
106
|
YAML_EXTENSIONS = %w[.yml .yaml].freeze
|
|
105
107
|
HTML_EXTENSIONS = %w[.html .htm].freeze
|
|
108
|
+
ERB_EXTENSIONS = %w[.erb].freeze
|
|
106
109
|
CSS_EXTENSIONS = %w[.css].freeze
|
|
107
110
|
SCSS_EXTENSIONS = %w[.scss].freeze
|
|
108
111
|
SQL_EXTENSIONS = %w[.sql].freeze
|
|
@@ -123,6 +126,8 @@ module Kward
|
|
|
123
126
|
editor_highlight_yaml(line)
|
|
124
127
|
when :html
|
|
125
128
|
editor_highlight_html(line)
|
|
129
|
+
when :erb
|
|
130
|
+
editor_highlight_erb(line, line_index)
|
|
126
131
|
when :css, :scss
|
|
127
132
|
editor_highlight_css(line)
|
|
128
133
|
when :sql
|
|
@@ -153,6 +158,7 @@ module Kward
|
|
|
153
158
|
return :json if JSON_EXTENSIONS.include?(extension)
|
|
154
159
|
return :yaml if YAML_EXTENSIONS.include?(extension)
|
|
155
160
|
return :html if HTML_EXTENSIONS.include?(extension)
|
|
161
|
+
return :erb if ERB_EXTENSIONS.include?(extension)
|
|
156
162
|
return :scss if SCSS_EXTENSIONS.include?(extension)
|
|
157
163
|
return :css if CSS_EXTENSIONS.include?(extension)
|
|
158
164
|
return :sql if SQL_EXTENSIONS.include?(extension)
|
|
@@ -174,6 +180,80 @@ module Kward
|
|
|
174
180
|
editor_highlight_ruby_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
|
|
175
181
|
end
|
|
176
182
|
|
|
183
|
+
def editor_highlight_erb(line, line_index = nil)
|
|
184
|
+
segments, = editor_erb_segments(line.to_s, editor_erb_state_before_line(line_index))
|
|
185
|
+
output = +""
|
|
186
|
+
comment = +""
|
|
187
|
+
html_in_tag = false
|
|
188
|
+
|
|
189
|
+
segments.each do |kind, text|
|
|
190
|
+
if kind == :comment
|
|
191
|
+
comment << text
|
|
192
|
+
next
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
unless comment.empty?
|
|
196
|
+
output << colored(comment, :gray)
|
|
197
|
+
comment.clear
|
|
198
|
+
end
|
|
199
|
+
case kind
|
|
200
|
+
when :template
|
|
201
|
+
highlighted, html_in_tag = editor_highlight_html_segment(text, in_tag: html_in_tag)
|
|
202
|
+
output << highlighted
|
|
203
|
+
when :ruby
|
|
204
|
+
output << editor_highlight_ruby(text)
|
|
205
|
+
when :delimiter
|
|
206
|
+
output << colored(text, :cyan)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
output << colored(comment, :gray) unless comment.empty?
|
|
210
|
+
output
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def editor_erb_state_before_line(line_index)
|
|
214
|
+
return :template unless line_index && @editor_state
|
|
215
|
+
|
|
216
|
+
state = :template
|
|
217
|
+
@editor_state.lines.first(line_index.to_i).each do |line|
|
|
218
|
+
_, state = editor_erb_segments(line.to_s, state)
|
|
219
|
+
end
|
|
220
|
+
state
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def editor_erb_segments(text, state)
|
|
224
|
+
segments = []
|
|
225
|
+
cursor = 0
|
|
226
|
+
|
|
227
|
+
while cursor < text.length
|
|
228
|
+
if state == :template
|
|
229
|
+
opening = text.match(ERB_OPEN_PATTERN, cursor)
|
|
230
|
+
if opening
|
|
231
|
+
segments << [:template, text[cursor...opening.begin(0)].to_s] unless opening.begin(0) == cursor
|
|
232
|
+
opening_kind = opening[0] == "<%#" ? :comment : :delimiter
|
|
233
|
+
segments << [opening_kind, opening[0]]
|
|
234
|
+
state = opening_kind == :comment ? :comment : :ruby
|
|
235
|
+
cursor = opening.end(0)
|
|
236
|
+
else
|
|
237
|
+
segments << [:template, text[cursor..].to_s]
|
|
238
|
+
break
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
closing = text.match(ERB_CLOSE_PATTERN, cursor)
|
|
242
|
+
if closing
|
|
243
|
+
segments << [state, text[cursor...closing.begin(0)].to_s] unless closing.begin(0) == cursor
|
|
244
|
+
segments << [state == :comment ? :comment : :delimiter, closing[0]]
|
|
245
|
+
state = :template
|
|
246
|
+
cursor = closing.end(0)
|
|
247
|
+
else
|
|
248
|
+
segments << [state, text[cursor..].to_s]
|
|
249
|
+
break
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
[segments, state]
|
|
255
|
+
end
|
|
256
|
+
|
|
177
257
|
def editor_highlight_ruby_code(line)
|
|
178
258
|
line.to_s.gsub(RUBY_PATTERN) do |token|
|
|
179
259
|
editor_highlight_ruby_token(token)
|
|
@@ -372,17 +452,112 @@ module Kward
|
|
|
372
452
|
end
|
|
373
453
|
|
|
374
454
|
def editor_highlight_html(line)
|
|
375
|
-
line.to_s
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
highlighted, = editor_highlight_html_segment(line.to_s)
|
|
456
|
+
highlighted
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def editor_highlight_html_segment(text, in_tag: false)
|
|
460
|
+
output = +""
|
|
461
|
+
cursor = 0
|
|
462
|
+
|
|
463
|
+
while cursor < text.length
|
|
464
|
+
if in_tag
|
|
465
|
+
close_index = text.index(">", cursor)
|
|
466
|
+
if close_index
|
|
467
|
+
output << editor_highlight_html_tag_fragment(text[cursor..close_index].to_s)
|
|
468
|
+
cursor = close_index + 1
|
|
469
|
+
in_tag = false
|
|
470
|
+
else
|
|
471
|
+
output << editor_highlight_html_tag_fragment(text[cursor..].to_s)
|
|
472
|
+
break
|
|
473
|
+
end
|
|
474
|
+
next
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
comment_index = text.index("<!--", cursor)
|
|
478
|
+
tag = text.match(HTML_TAG_START_PATTERN, cursor)
|
|
479
|
+
next_match = [comment_index, tag&.begin(0)].compact.min
|
|
480
|
+
unless next_match
|
|
481
|
+
output << text[cursor..].to_s
|
|
482
|
+
break
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
output << text[cursor...next_match].to_s if next_match > cursor
|
|
486
|
+
if comment_index == next_match
|
|
487
|
+
close_index = text.index("-->", comment_index + 4)
|
|
488
|
+
end_index = close_index ? close_index + 3 : text.length
|
|
489
|
+
output << colored(text[comment_index...end_index].to_s, :gray)
|
|
490
|
+
cursor = end_index
|
|
382
491
|
else
|
|
383
|
-
|
|
492
|
+
close_index = text.index(">", tag.end(0))
|
|
493
|
+
end_index = close_index ? close_index + 1 : text.length
|
|
494
|
+
output << editor_highlight_html_tag_fragment(text[tag.begin(0)...end_index].to_s)
|
|
495
|
+
cursor = end_index
|
|
496
|
+
in_tag = close_index.nil?
|
|
384
497
|
end
|
|
385
498
|
end
|
|
499
|
+
|
|
500
|
+
[output, in_tag]
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def editor_highlight_html_tag_fragment(tag)
|
|
504
|
+
output = +""
|
|
505
|
+
cursor = 0
|
|
506
|
+
head = tag.match(/\A<\/?[A-Za-z][\w:-]*/)
|
|
507
|
+
if head
|
|
508
|
+
output << colored(head[0], :blue)
|
|
509
|
+
cursor = head.end(0)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
while cursor < tag.length
|
|
513
|
+
rest = tag[cursor..].to_s
|
|
514
|
+
if rest.start_with?(">", "/>")
|
|
515
|
+
output << colored(rest.start_with?("/>") ? "/>" : ">", :blue)
|
|
516
|
+
cursor += rest.start_with?("/>") ? 2 : 1
|
|
517
|
+
elsif rest.match?(/\A\s/)
|
|
518
|
+
whitespace = rest[/\A\s+/]
|
|
519
|
+
output << whitespace
|
|
520
|
+
cursor += whitespace.length
|
|
521
|
+
elsif rest.start_with?("\"", "'")
|
|
522
|
+
quote = rest[0]
|
|
523
|
+
close_index = rest.index(quote, 1)
|
|
524
|
+
if close_index
|
|
525
|
+
end_index = close_index + 1
|
|
526
|
+
output << colored(rest[0...end_index], :green)
|
|
527
|
+
cursor += end_index
|
|
528
|
+
elsif rest.end_with?(">") && rest.length > 1
|
|
529
|
+
output << colored(rest[0...-1], :green)
|
|
530
|
+
output << colored(">", :blue)
|
|
531
|
+
cursor += rest.length
|
|
532
|
+
else
|
|
533
|
+
output << colored(rest, :green)
|
|
534
|
+
cursor += rest.length
|
|
535
|
+
end
|
|
536
|
+
elsif (attribute = rest.match(/\A([A-Za-z_:][\w:.-]*)(\s*=\s*)/))
|
|
537
|
+
output << colored(attribute[1], :cyan)
|
|
538
|
+
output << attribute[2]
|
|
539
|
+
cursor += attribute[0].length
|
|
540
|
+
value = tag[cursor..].to_s
|
|
541
|
+
if value.start_with?("\"", "'")
|
|
542
|
+
quote = value[0]
|
|
543
|
+
close_index = value.index(quote, 1)
|
|
544
|
+
end_index = close_index ? close_index + 1 : value.length
|
|
545
|
+
output << colored(value[0...end_index], :green)
|
|
546
|
+
cursor += end_index
|
|
547
|
+
elsif (unquoted = value.match(/\A[^\s>]+/))
|
|
548
|
+
output << colored(unquoted[0], :green)
|
|
549
|
+
cursor += unquoted[0].length
|
|
550
|
+
end
|
|
551
|
+
elsif (attribute = rest.match(/\A[A-Za-z_:][\w:.-]*/))
|
|
552
|
+
output << colored(attribute[0], :cyan)
|
|
553
|
+
cursor += attribute[0].length
|
|
554
|
+
else
|
|
555
|
+
output << rest[0]
|
|
556
|
+
cursor += 1
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
output
|
|
386
561
|
end
|
|
387
562
|
|
|
388
563
|
def editor_highlight_css(line)
|
|
@@ -133,11 +133,11 @@ module Kward
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def git_project_file_paths
|
|
136
|
-
ProjectFiles.git_paths(
|
|
136
|
+
ProjectFiles.git_paths(prompt_workspace_root)
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def scanned_project_file_paths
|
|
140
|
-
ProjectFiles.scanned_paths(
|
|
140
|
+
ProjectFiles.scanned_paths(prompt_workspace_root)
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def selected_file_mention_path
|
|
@@ -84,6 +84,8 @@ module Kward
|
|
|
84
84
|
binding_result = handle_composer_key_binding(key) if git_composing?
|
|
85
85
|
return binding_result unless binding_result == false || binding_result.nil?
|
|
86
86
|
|
|
87
|
+
return git_move_selection(1) if key == "j" && !git_composing?
|
|
88
|
+
return git_move_selection(-1) if key == "k" && !git_composing?
|
|
87
89
|
return git_toggle_selected_file if key == "s" && !git_composing?
|
|
88
90
|
|
|
89
91
|
insert_key(key) if git_composing?
|
|
@@ -111,8 +113,10 @@ module Kward
|
|
|
111
113
|
delete_at_cursor if git_composing?
|
|
112
114
|
nil
|
|
113
115
|
else
|
|
114
|
-
|
|
115
|
-
return
|
|
116
|
+
unless git_composing?
|
|
117
|
+
return git_move_selection(1) if code == "j".ord
|
|
118
|
+
return git_move_selection(-1) if code == "k".ord
|
|
119
|
+
return git_toggle_selected_file if code == "s".ord && (sequence[:modifiers].to_s.empty? || sequence[:modifiers].to_s == "1")
|
|
116
120
|
end
|
|
117
121
|
return false unless git_composing?
|
|
118
122
|
|
|
@@ -265,7 +269,7 @@ module Kward
|
|
|
265
269
|
def git_overlay_rows(width, height: screen_height)
|
|
266
270
|
return [] unless @git_state
|
|
267
271
|
|
|
268
|
-
help = git_composing? ? "Type commit message · Enter commit · Tab overlay · Esc cancel" : "
|
|
272
|
+
help = git_composing? ? "Type commit message · Enter commit · Tab overlay · Esc cancel" : "↑/↓/j/k select · Enter diff · s stage/unstage · Tab message · Esc cancel"
|
|
269
273
|
lines = [overlay_text_line(help, :muted), overlay_blank_line]
|
|
270
274
|
status_lines = @git_state[:status_lines]
|
|
271
275
|
status_lines = ["No uncommitted changes."] if status_lines.empty?
|
|
@@ -45,7 +45,8 @@ module Kward
|
|
|
45
45
|
@on_tick = block
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
#
|
|
48
|
+
# Stages a character at the given canvas position with optional color.
|
|
49
|
+
# Call {#render} after completing the frame to publish it.
|
|
49
50
|
#
|
|
50
51
|
# @param row [Integer] zero-based row
|
|
51
52
|
# @param col [Integer] zero-based column
|
|
@@ -59,15 +60,14 @@ module Kward
|
|
|
59
60
|
return if col.negative? || col >= @width
|
|
60
61
|
|
|
61
62
|
@cells[row][col] = { char: char.to_s[0] || " ", colors: colors.flatten }
|
|
62
|
-
@dirty = true
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
#
|
|
65
|
+
# Stages a reset of all canvas cells to blank. Call {#render} after
|
|
66
|
+
# completing the frame to publish it.
|
|
66
67
|
#
|
|
67
68
|
# @return [void]
|
|
68
69
|
def clear_frame
|
|
69
70
|
@cells = Array.new(@height) { Array.new(@width) { blank_cell } }
|
|
70
|
-
@dirty = true
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Marks the canvas as ready for Kward to render. Called after the plugin
|
|
@@ -55,6 +55,9 @@ module Kward
|
|
|
55
55
|
controller = @interactive_state[:controller]
|
|
56
56
|
return false if controller.exited?
|
|
57
57
|
|
|
58
|
+
return true if controller.dirty?
|
|
59
|
+
return false unless controller.tickable?
|
|
60
|
+
|
|
58
61
|
now = monotonic_now
|
|
59
62
|
interval = 1.0 / controller.fps
|
|
60
63
|
elapsed = now - @last_interactive_tick
|
|
@@ -64,7 +67,7 @@ module Kward
|
|
|
64
67
|
@last_interactive_tick += steps * interval
|
|
65
68
|
result = controller.invoke_tick
|
|
66
69
|
controller.exit if result == :exit
|
|
67
|
-
|
|
70
|
+
controller.dirty?
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
end
|
|
@@ -125,6 +125,14 @@ module Kward
|
|
|
125
125
|
insert_selected_project_browser_mention
|
|
126
126
|
elsif text == "/" && !project_browser_search_active?
|
|
127
127
|
activate_project_browser_search
|
|
128
|
+
elsif text == "j" && !project_browser_search_active?
|
|
129
|
+
select_next_project_browser_row
|
|
130
|
+
elsif text == "k" && !project_browser_search_active?
|
|
131
|
+
select_previous_project_browser_row
|
|
132
|
+
elsif text == "h" && !project_browser_search_active?
|
|
133
|
+
collapse_selected_project_browser_row
|
|
134
|
+
elsif text == "l" && !project_browser_search_active?
|
|
135
|
+
expand_selected_project_browser_row
|
|
128
136
|
elsif project_browser_search_active?
|
|
129
137
|
project_browser_append_search(text)
|
|
130
138
|
else
|
|
@@ -148,6 +156,14 @@ module Kward
|
|
|
148
156
|
toggle_project_browser_search
|
|
149
157
|
when "/"
|
|
150
158
|
activate_project_browser_search
|
|
159
|
+
when "j"
|
|
160
|
+
project_browser_search_active? ? project_browser_append_search(key) : select_next_project_browser_row
|
|
161
|
+
when "k"
|
|
162
|
+
project_browser_search_active? ? project_browser_append_search(key) : select_previous_project_browser_row
|
|
163
|
+
when "h"
|
|
164
|
+
project_browser_search_active? ? project_browser_append_search(key) : collapse_selected_project_browser_row
|
|
165
|
+
when "l"
|
|
166
|
+
project_browser_search_active? ? project_browser_append_search(key) : expand_selected_project_browser_row
|
|
151
167
|
else
|
|
152
168
|
project_browser_append_search(key) if project_browser_search_active? && printable_key?(key)
|
|
153
169
|
end
|
|
@@ -319,7 +335,7 @@ module Kward
|
|
|
319
335
|
if rows.empty?
|
|
320
336
|
lines << overlay_text_line(project_browser_empty_message, :muted)
|
|
321
337
|
else
|
|
322
|
-
visible = visible_project_browser_rows(rows, height: height)
|
|
338
|
+
visible = visible_project_browser_rows(rows, height: height, width: width)
|
|
323
339
|
visible[:rows].each_with_index do |row, offset|
|
|
324
340
|
index = visible[:start] + offset
|
|
325
341
|
lines << overlay_choice_line(project_browser_row_text(row), selected: index == @project_browser_state[:selection_index])
|
|
@@ -476,7 +492,7 @@ module Kward
|
|
|
476
492
|
end
|
|
477
493
|
|
|
478
494
|
def project_browser_workspace_root
|
|
479
|
-
ConfigFiles.canonical_workspace_root(
|
|
495
|
+
ConfigFiles.canonical_workspace_root(prompt_workspace_root)
|
|
480
496
|
end
|
|
481
497
|
|
|
482
498
|
def restored_project_browser_expanded_paths(paths, saved_state)
|
|
@@ -536,14 +552,17 @@ module Kward
|
|
|
536
552
|
@project_browser_state[:selection_index] = [[@project_browser_state[:selection_index], 0].max, rows.length - 1].min unless rows.empty?
|
|
537
553
|
end
|
|
538
554
|
|
|
539
|
-
def visible_project_browser_rows(rows, height: screen_height)
|
|
540
|
-
max_rows = max_project_browser_rows(height)
|
|
555
|
+
def visible_project_browser_rows(rows, height: screen_height, width: screen_width)
|
|
556
|
+
max_rows = max_project_browser_rows(height, width: width)
|
|
541
557
|
start = centered_list_window_start(@project_browser_state[:selection_index], rows.length, max_rows)
|
|
542
558
|
{ start: start, rows: rows[start, max_rows] || [] }
|
|
543
559
|
end
|
|
544
560
|
|
|
545
|
-
def max_project_browser_rows(height)
|
|
546
|
-
|
|
561
|
+
def max_project_browser_rows(height, width: screen_width)
|
|
562
|
+
composer_bottom_rows = @tabs.empty? ? 1 : tab_border_rows(width).length
|
|
563
|
+
footer_rows = footer_text.empty? ? 0 : 1
|
|
564
|
+
attachment_rows = attachment_badge_texts.length
|
|
565
|
+
[height - 7 - composer_bottom_rows - footer_rows - attachment_rows, 4].max
|
|
547
566
|
end
|
|
548
567
|
|
|
549
568
|
def default_project_browser_expanded_paths(paths)
|
|
@@ -40,14 +40,30 @@ module Kward
|
|
|
40
40
|
|
|
41
41
|
def tick_footer_locked
|
|
42
42
|
return false unless @footer && @started && @asking
|
|
43
|
+
return false unless footer_refresh_due?
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
previous_text = @cached_footer_text
|
|
46
|
+
refresh_footer_text_locked
|
|
47
|
+
previous_text != @cached_footer_text
|
|
48
|
+
end
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
def cached_footer_text
|
|
51
|
+
return "" unless @footer
|
|
52
|
+
|
|
53
|
+
refresh_footer_text_locked if footer_refresh_due?
|
|
54
|
+
@cached_footer_text.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def footer_refresh_due?
|
|
58
|
+
@last_footer_refresh.nil? || monotonic_now - @last_footer_refresh >= FOOTER_REFRESH_INTERVAL
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def refresh_footer_text_locked
|
|
62
|
+
@cached_footer_text = @footer.call.to_s.gsub(/\s+/, " ").strip
|
|
63
|
+
rescue StandardError
|
|
64
|
+
@cached_footer_text = ""
|
|
65
|
+
ensure
|
|
66
|
+
@last_footer_refresh = monotonic_now
|
|
51
67
|
end
|
|
52
68
|
|
|
53
69
|
def cached_composer_status_text
|
|
@@ -114,6 +114,9 @@ module Kward
|
|
|
114
114
|
@reserved_rows = 0
|
|
115
115
|
@last_width = current_width
|
|
116
116
|
@last_height = current_height
|
|
117
|
+
if interactive_active_locked?
|
|
118
|
+
@interactive_state[:controller].resize(width: interactive_canvas_width(current_width))
|
|
119
|
+
end
|
|
117
120
|
redraw_screen_locked(width: current_width, height: current_height)
|
|
118
121
|
true
|
|
119
122
|
end
|
|
@@ -626,7 +626,13 @@ module Kward
|
|
|
626
626
|
|
|
627
627
|
def selection_matches
|
|
628
628
|
choices = @select_state ? @select_state[:choices] : []
|
|
629
|
-
filter =
|
|
629
|
+
filter = if select_input_active?
|
|
630
|
+
""
|
|
631
|
+
elsif select_search_active? || !composer_input.strip.empty?
|
|
632
|
+
composer_input.downcase.strip
|
|
633
|
+
else
|
|
634
|
+
""
|
|
635
|
+
end
|
|
630
636
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
631
637
|
clamp_selection_index(matches.length)
|
|
632
638
|
matches
|