kward 0.79.0 → 0.80.1
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 +65 -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/skills.md +2 -0
- 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/agent.rb +11 -6
- 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/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +44 -1
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +5 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/slash_commands.rb +1 -1
- data/lib/kward/cli/tabs.rb +103 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -0
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +51 -12
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/hooks/audit_log.rb +5 -2
- 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 +99 -19
- 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 +219 -19
- 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/templates.rb +11 -6
- 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/skills/registry.rb +16 -11
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/telemetry/logger.rb +5 -2
- 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
|
@@ -323,6 +323,8 @@ module Kward
|
|
|
323
323
|
code.match?(/:\s*(?:[#].*)?\z/)
|
|
324
324
|
when :html
|
|
325
325
|
editor_html_line_opens_indent?(code)
|
|
326
|
+
when :erb
|
|
327
|
+
editor_erb_line_opens_indent?(code)
|
|
326
328
|
when *C_LIKE_INDENT_LANGUAGES
|
|
327
329
|
false
|
|
328
330
|
else
|
|
@@ -364,6 +366,46 @@ module Kward
|
|
|
364
366
|
true
|
|
365
367
|
end
|
|
366
368
|
|
|
369
|
+
def editor_erb_line_opens_indent?(code)
|
|
370
|
+
segments, = editor_erb_segments(code.to_s, :template)
|
|
371
|
+
segments.any? do |kind, text|
|
|
372
|
+
case kind
|
|
373
|
+
when :ruby
|
|
374
|
+
editor_ruby_line_opens_indent?(text.strip)
|
|
375
|
+
when :template
|
|
376
|
+
editor_html_line_opens_indent?(text.strip)
|
|
377
|
+
else
|
|
378
|
+
false
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def editor_erb_line_closer?(code)
|
|
384
|
+
segments, = editor_erb_segments(code.to_s, :template)
|
|
385
|
+
segments.any? do |kind, text|
|
|
386
|
+
case kind
|
|
387
|
+
when :ruby
|
|
388
|
+
text.strip.match?(/\Aend\z/)
|
|
389
|
+
when :template
|
|
390
|
+
text.strip.match?(/\A<\/[A-Za-z][\w:-]*>\z/)
|
|
391
|
+
else
|
|
392
|
+
false
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def editor_matching_erb_indent(line_index = nil)
|
|
398
|
+
stack = []
|
|
399
|
+
editor_previous_code_lines(line_index).each do |line|
|
|
400
|
+
code = line[:code].strip
|
|
401
|
+
next if code.empty?
|
|
402
|
+
|
|
403
|
+
stack.pop if editor_erb_line_closer?(code)
|
|
404
|
+
stack << line[:indent] if editor_erb_line_opens_indent?(code)
|
|
405
|
+
end
|
|
406
|
+
stack.last || ""
|
|
407
|
+
end
|
|
408
|
+
|
|
367
409
|
def editor_closing_punctuation?(text)
|
|
368
410
|
PUNCTUATION_PAIRS.key?(text) && PUNCTUATION_INDENT_LANGUAGES.include?(editor_syntax_language)
|
|
369
411
|
end
|
|
@@ -424,6 +466,8 @@ module Kward
|
|
|
424
466
|
code.match?(/\A[ \t]*(?:#{Regexp.union(SHELL_DEDENT_KEYWORDS)})\z/)
|
|
425
467
|
when :html
|
|
426
468
|
code.match?(/\A[ \t]*<\/[A-Za-z][\w:-]*>\z/)
|
|
469
|
+
when :erb
|
|
470
|
+
editor_erb_line_closer?(code)
|
|
427
471
|
else
|
|
428
472
|
false
|
|
429
473
|
end
|
|
@@ -439,6 +483,8 @@ module Kward
|
|
|
439
483
|
editor_matching_keyword_indent(nil, SHELL_DEDENT_KEYWORDS, line_index)
|
|
440
484
|
when :html
|
|
441
485
|
editor_matching_html_indent(line_index)
|
|
486
|
+
when :erb
|
|
487
|
+
editor_matching_erb_indent(line_index)
|
|
442
488
|
end
|
|
443
489
|
end
|
|
444
490
|
|
|
@@ -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,20 +11,24 @@ 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: {
|
|
23
25
|
extensions: %w[.js .jsx .mjs .cjs],
|
|
26
|
+
block_comment: true,
|
|
24
27
|
keywords: %w[async await break case catch class const continue debugger default delete do else export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield]
|
|
25
28
|
},
|
|
26
29
|
typescript: {
|
|
27
30
|
extensions: %w[.ts .tsx],
|
|
31
|
+
block_comment: true,
|
|
28
32
|
keywords: %w[abstract any as async await boolean break case catch class const constructor continue debugger declare default delete do else enum export extends false finally for from function if implements import in infer instanceof interface is keyof let module namespace never new null number object of private protected public readonly return static string super switch symbol this throw true try type typeof undefined unknown var void while with yield]
|
|
29
33
|
},
|
|
30
34
|
shell: {
|
|
@@ -60,34 +64,42 @@ module Kward
|
|
|
60
64
|
},
|
|
61
65
|
go: {
|
|
62
66
|
extensions: %w[.go],
|
|
67
|
+
block_comment: true,
|
|
63
68
|
keywords: %w[break case chan const continue default defer else fallthrough false for func go goto if import interface map nil package range return select struct switch true type var]
|
|
64
69
|
},
|
|
65
70
|
rust: {
|
|
66
71
|
extensions: %w[.rs],
|
|
72
|
+
block_comment: true,
|
|
67
73
|
keywords: %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while]
|
|
68
74
|
},
|
|
69
75
|
java: {
|
|
70
76
|
extensions: %w[.java],
|
|
77
|
+
block_comment: true,
|
|
71
78
|
keywords: %w[abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient true try void volatile while]
|
|
72
79
|
},
|
|
73
80
|
csharp: {
|
|
74
81
|
extensions: %w[.cs],
|
|
82
|
+
block_comment: true,
|
|
75
83
|
keywords: %w[abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while var async await]
|
|
76
84
|
},
|
|
77
85
|
c: {
|
|
78
86
|
extensions: %w[.c .h],
|
|
87
|
+
block_comment: true,
|
|
79
88
|
keywords: %w[auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while]
|
|
80
89
|
},
|
|
81
90
|
cpp: {
|
|
82
91
|
extensions: %w[.cc .cpp .cxx .hpp .hh .hxx],
|
|
92
|
+
block_comment: true,
|
|
83
93
|
keywords: %w[alignas alignof and asm auto bool break case catch char char16_t char32_t class const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept nullptr operator or private protected public register reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while]
|
|
84
94
|
},
|
|
85
95
|
swift: {
|
|
86
96
|
extensions: %w[.swift],
|
|
97
|
+
block_comment: true,
|
|
87
98
|
keywords: %w[as associatedtype break case catch class continue default defer deinit do else enum extension false fileprivate for func guard if import in init inout internal is let nil open operator private protocol public repeat rethrows return self Self static struct subscript super switch throw throws true try typealias var where while]
|
|
88
99
|
},
|
|
89
100
|
kotlin: {
|
|
90
101
|
extensions: %w[.kt .kts],
|
|
102
|
+
block_comment: true,
|
|
91
103
|
keywords: %w[as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while by catch constructor delegate dynamic field file finally get import init param property receiver set setparam where actual abstract annotation companion const crossinline data enum expect external final infix inline inner internal lateinit noinline open operator out override private protected public reified sealed suspend tailrec vararg]
|
|
92
104
|
},
|
|
93
105
|
lua: {
|
|
@@ -103,6 +115,7 @@ module Kward
|
|
|
103
115
|
JSON_EXTENSIONS = %w[.json].freeze
|
|
104
116
|
YAML_EXTENSIONS = %w[.yml .yaml].freeze
|
|
105
117
|
HTML_EXTENSIONS = %w[.html .htm].freeze
|
|
118
|
+
ERB_EXTENSIONS = %w[.erb].freeze
|
|
106
119
|
CSS_EXTENSIONS = %w[.css].freeze
|
|
107
120
|
SCSS_EXTENSIONS = %w[.scss].freeze
|
|
108
121
|
SQL_EXTENSIONS = %w[.sql].freeze
|
|
@@ -123,6 +136,8 @@ module Kward
|
|
|
123
136
|
editor_highlight_yaml(line)
|
|
124
137
|
when :html
|
|
125
138
|
editor_highlight_html(line)
|
|
139
|
+
when :erb
|
|
140
|
+
editor_highlight_erb(line, line_index)
|
|
126
141
|
when :css, :scss
|
|
127
142
|
editor_highlight_css(line)
|
|
128
143
|
when :sql
|
|
@@ -153,6 +168,7 @@ module Kward
|
|
|
153
168
|
return :json if JSON_EXTENSIONS.include?(extension)
|
|
154
169
|
return :yaml if YAML_EXTENSIONS.include?(extension)
|
|
155
170
|
return :html if HTML_EXTENSIONS.include?(extension)
|
|
171
|
+
return :erb if ERB_EXTENSIONS.include?(extension)
|
|
156
172
|
return :scss if SCSS_EXTENSIONS.include?(extension)
|
|
157
173
|
return :css if CSS_EXTENSIONS.include?(extension)
|
|
158
174
|
return :sql if SQL_EXTENSIONS.include?(extension)
|
|
@@ -174,6 +190,80 @@ module Kward
|
|
|
174
190
|
editor_highlight_ruby_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
|
|
175
191
|
end
|
|
176
192
|
|
|
193
|
+
def editor_highlight_erb(line, line_index = nil)
|
|
194
|
+
segments, = editor_erb_segments(line.to_s, editor_erb_state_before_line(line_index))
|
|
195
|
+
output = +""
|
|
196
|
+
comment = +""
|
|
197
|
+
html_in_tag = false
|
|
198
|
+
|
|
199
|
+
segments.each do |kind, text|
|
|
200
|
+
if kind == :comment
|
|
201
|
+
comment << text
|
|
202
|
+
next
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
unless comment.empty?
|
|
206
|
+
output << colored(comment, :gray)
|
|
207
|
+
comment.clear
|
|
208
|
+
end
|
|
209
|
+
case kind
|
|
210
|
+
when :template
|
|
211
|
+
highlighted, html_in_tag = editor_highlight_html_segment(text, in_tag: html_in_tag)
|
|
212
|
+
output << highlighted
|
|
213
|
+
when :ruby
|
|
214
|
+
output << editor_highlight_ruby(text)
|
|
215
|
+
when :delimiter
|
|
216
|
+
output << colored(text, :cyan)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
output << colored(comment, :gray) unless comment.empty?
|
|
220
|
+
output
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def editor_erb_state_before_line(line_index)
|
|
224
|
+
return :template unless line_index && @editor_state
|
|
225
|
+
|
|
226
|
+
state = :template
|
|
227
|
+
@editor_state.lines.first(line_index.to_i).each do |line|
|
|
228
|
+
_, state = editor_erb_segments(line.to_s, state)
|
|
229
|
+
end
|
|
230
|
+
state
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def editor_erb_segments(text, state)
|
|
234
|
+
segments = []
|
|
235
|
+
cursor = 0
|
|
236
|
+
|
|
237
|
+
while cursor < text.length
|
|
238
|
+
if state == :template
|
|
239
|
+
opening = text.match(ERB_OPEN_PATTERN, cursor)
|
|
240
|
+
if opening
|
|
241
|
+
segments << [:template, text[cursor...opening.begin(0)].to_s] unless opening.begin(0) == cursor
|
|
242
|
+
opening_kind = opening[0] == "<%#" ? :comment : :delimiter
|
|
243
|
+
segments << [opening_kind, opening[0]]
|
|
244
|
+
state = opening_kind == :comment ? :comment : :ruby
|
|
245
|
+
cursor = opening.end(0)
|
|
246
|
+
else
|
|
247
|
+
segments << [:template, text[cursor..].to_s]
|
|
248
|
+
break
|
|
249
|
+
end
|
|
250
|
+
else
|
|
251
|
+
closing = text.match(ERB_CLOSE_PATTERN, cursor)
|
|
252
|
+
if closing
|
|
253
|
+
segments << [state, text[cursor...closing.begin(0)].to_s] unless closing.begin(0) == cursor
|
|
254
|
+
segments << [state == :comment ? :comment : :delimiter, closing[0]]
|
|
255
|
+
state = :template
|
|
256
|
+
cursor = closing.end(0)
|
|
257
|
+
else
|
|
258
|
+
segments << [state, text[cursor..].to_s]
|
|
259
|
+
break
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
[segments, state]
|
|
265
|
+
end
|
|
266
|
+
|
|
177
267
|
def editor_highlight_ruby_code(line)
|
|
178
268
|
line.to_s.gsub(RUBY_PATTERN) do |token|
|
|
179
269
|
editor_highlight_ruby_token(token)
|
|
@@ -216,8 +306,7 @@ module Kward
|
|
|
216
306
|
return line.to_s unless definition
|
|
217
307
|
|
|
218
308
|
text = line.to_s
|
|
219
|
-
return colored(text, :gray) if editor_c_style_block_comment_line?(line_index)
|
|
220
|
-
|
|
309
|
+
return colored(text, :gray) if definition[:block_comment] && editor_c_style_block_comment_line?(line_index)
|
|
221
310
|
marker = definition[:line_comment] || "//"
|
|
222
311
|
comment_index = editor_comment_index(text, marker)
|
|
223
312
|
return editor_highlight_generic_code(text, definition[:keywords]) unless comment_index
|
|
@@ -233,7 +322,8 @@ module Kward
|
|
|
233
322
|
end
|
|
234
323
|
|
|
235
324
|
def editor_generic_pattern(keywords)
|
|
236
|
-
|
|
325
|
+
@editor_generic_patterns ||= {}
|
|
326
|
+
@editor_generic_patterns[keywords] ||= /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(keywords)})\b)/
|
|
237
327
|
end
|
|
238
328
|
|
|
239
329
|
def editor_highlight_generic_token(token, keywords)
|
|
@@ -253,16 +343,31 @@ module Kward
|
|
|
253
343
|
def editor_c_style_block_comment_line?(line_index)
|
|
254
344
|
return false unless line_index && @editor_state
|
|
255
345
|
|
|
256
|
-
|
|
257
|
-
@
|
|
346
|
+
lines = @editor_state.lines
|
|
347
|
+
unless @editor_block_comment_lines.equal?(lines)
|
|
348
|
+
@editor_block_comment_lines = lines
|
|
349
|
+
@editor_block_comment_states = []
|
|
350
|
+
@editor_block_comment_in_comment = false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
target = line_index.to_i
|
|
354
|
+
return false if target.negative?
|
|
355
|
+
return @editor_block_comment_states[target] if target < @editor_block_comment_states.length
|
|
356
|
+
|
|
357
|
+
(@editor_block_comment_states.length..target).each do |index|
|
|
358
|
+
line = lines[index].to_s
|
|
258
359
|
starts_block = editor_comment_index(line, "/*")
|
|
259
|
-
ends_block =
|
|
260
|
-
|
|
360
|
+
ends_block = @editor_block_comment_in_comment && line.include?("*/")
|
|
361
|
+
@editor_block_comment_states << (@editor_block_comment_in_comment || !starts_block.nil?)
|
|
261
362
|
|
|
262
|
-
|
|
263
|
-
|
|
363
|
+
if starts_block && !line[starts_block..].to_s.include?("*/")
|
|
364
|
+
@editor_block_comment_in_comment = true
|
|
365
|
+
elsif ends_block
|
|
366
|
+
@editor_block_comment_in_comment = false
|
|
367
|
+
end
|
|
264
368
|
end
|
|
265
|
-
|
|
369
|
+
|
|
370
|
+
@editor_block_comment_states[target]
|
|
266
371
|
end
|
|
267
372
|
|
|
268
373
|
def editor_comment_index(line, marker)
|
|
@@ -372,17 +477,112 @@ module Kward
|
|
|
372
477
|
end
|
|
373
478
|
|
|
374
479
|
def editor_highlight_html(line)
|
|
375
|
-
line.to_s
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
480
|
+
highlighted, = editor_highlight_html_segment(line.to_s)
|
|
481
|
+
highlighted
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def editor_highlight_html_segment(text, in_tag: false)
|
|
485
|
+
output = +""
|
|
486
|
+
cursor = 0
|
|
487
|
+
|
|
488
|
+
while cursor < text.length
|
|
489
|
+
if in_tag
|
|
490
|
+
close_index = text.index(">", cursor)
|
|
491
|
+
if close_index
|
|
492
|
+
output << editor_highlight_html_tag_fragment(text[cursor..close_index].to_s)
|
|
493
|
+
cursor = close_index + 1
|
|
494
|
+
in_tag = false
|
|
495
|
+
else
|
|
496
|
+
output << editor_highlight_html_tag_fragment(text[cursor..].to_s)
|
|
497
|
+
break
|
|
498
|
+
end
|
|
499
|
+
next
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
comment_index = text.index("<!--", cursor)
|
|
503
|
+
tag = text.match(HTML_TAG_START_PATTERN, cursor)
|
|
504
|
+
next_match = [comment_index, tag&.begin(0)].compact.min
|
|
505
|
+
unless next_match
|
|
506
|
+
output << text[cursor..].to_s
|
|
507
|
+
break
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
output << text[cursor...next_match].to_s if next_match > cursor
|
|
511
|
+
if comment_index == next_match
|
|
512
|
+
close_index = text.index("-->", comment_index + 4)
|
|
513
|
+
end_index = close_index ? close_index + 3 : text.length
|
|
514
|
+
output << colored(text[comment_index...end_index].to_s, :gray)
|
|
515
|
+
cursor = end_index
|
|
382
516
|
else
|
|
383
|
-
|
|
517
|
+
close_index = text.index(">", tag.end(0))
|
|
518
|
+
end_index = close_index ? close_index + 1 : text.length
|
|
519
|
+
output << editor_highlight_html_tag_fragment(text[tag.begin(0)...end_index].to_s)
|
|
520
|
+
cursor = end_index
|
|
521
|
+
in_tag = close_index.nil?
|
|
384
522
|
end
|
|
385
523
|
end
|
|
524
|
+
|
|
525
|
+
[output, in_tag]
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def editor_highlight_html_tag_fragment(tag)
|
|
529
|
+
output = +""
|
|
530
|
+
cursor = 0
|
|
531
|
+
head = tag.match(/\A<\/?[A-Za-z][\w:-]*/)
|
|
532
|
+
if head
|
|
533
|
+
output << colored(head[0], :blue)
|
|
534
|
+
cursor = head.end(0)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
while cursor < tag.length
|
|
538
|
+
rest = tag[cursor..].to_s
|
|
539
|
+
if rest.start_with?(">", "/>")
|
|
540
|
+
output << colored(rest.start_with?("/>") ? "/>" : ">", :blue)
|
|
541
|
+
cursor += rest.start_with?("/>") ? 2 : 1
|
|
542
|
+
elsif rest.match?(/\A\s/)
|
|
543
|
+
whitespace = rest[/\A\s+/]
|
|
544
|
+
output << whitespace
|
|
545
|
+
cursor += whitespace.length
|
|
546
|
+
elsif rest.start_with?("\"", "'")
|
|
547
|
+
quote = rest[0]
|
|
548
|
+
close_index = rest.index(quote, 1)
|
|
549
|
+
if close_index
|
|
550
|
+
end_index = close_index + 1
|
|
551
|
+
output << colored(rest[0...end_index], :green)
|
|
552
|
+
cursor += end_index
|
|
553
|
+
elsif rest.end_with?(">") && rest.length > 1
|
|
554
|
+
output << colored(rest[0...-1], :green)
|
|
555
|
+
output << colored(">", :blue)
|
|
556
|
+
cursor += rest.length
|
|
557
|
+
else
|
|
558
|
+
output << colored(rest, :green)
|
|
559
|
+
cursor += rest.length
|
|
560
|
+
end
|
|
561
|
+
elsif (attribute = rest.match(/\A([A-Za-z_:][\w:.-]*)(\s*=\s*)/))
|
|
562
|
+
output << colored(attribute[1], :cyan)
|
|
563
|
+
output << attribute[2]
|
|
564
|
+
cursor += attribute[0].length
|
|
565
|
+
value = tag[cursor..].to_s
|
|
566
|
+
if value.start_with?("\"", "'")
|
|
567
|
+
quote = value[0]
|
|
568
|
+
close_index = value.index(quote, 1)
|
|
569
|
+
end_index = close_index ? close_index + 1 : value.length
|
|
570
|
+
output << colored(value[0...end_index], :green)
|
|
571
|
+
cursor += end_index
|
|
572
|
+
elsif (unquoted = value.match(/\A[^\s>]+/))
|
|
573
|
+
output << colored(unquoted[0], :green)
|
|
574
|
+
cursor += unquoted[0].length
|
|
575
|
+
end
|
|
576
|
+
elsif (attribute = rest.match(/\A[A-Za-z_:][\w:.-]*/))
|
|
577
|
+
output << colored(attribute[0], :cyan)
|
|
578
|
+
cursor += attribute[0].length
|
|
579
|
+
else
|
|
580
|
+
output << rest[0]
|
|
581
|
+
cursor += 1
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
output
|
|
386
586
|
end
|
|
387
587
|
|
|
388
588
|
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
|