kward 0.82.0 → 0.84.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 +96 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +144 -21
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +21 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +44 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
data/lib/kward/project_files.rb
CHANGED
|
@@ -8,21 +8,34 @@ module Kward
|
|
|
8
8
|
module ProjectFiles
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
-
def list(root: Dir.pwd)
|
|
12
|
-
paths = git_paths(root)
|
|
13
|
-
paths = scanned_paths(root) if paths.empty?
|
|
11
|
+
def list(root: Dir.pwd, include_ignored: false)
|
|
12
|
+
paths = git_paths(root, include_ignored: include_ignored)
|
|
13
|
+
paths = scanned_paths(root) if paths.empty? && !git_repository?(root)
|
|
14
14
|
paths.reject { |path| path.empty? || path.end_with?("/") }.uniq.sort
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def git_paths(root)
|
|
18
|
-
output, status = Open3.
|
|
17
|
+
def git_paths(root, include_ignored: false)
|
|
18
|
+
output, status = Open3.capture2e("git", "ls-files", "--cached", "--others", "--exclude-standard", chdir: root)
|
|
19
19
|
return [] unless status.success?
|
|
20
20
|
|
|
21
|
-
output.lines.map(&:chomp).reject(&:empty?)
|
|
21
|
+
paths = output.lines.map(&:chomp).reject(&:empty?)
|
|
22
|
+
return paths unless include_ignored
|
|
23
|
+
|
|
24
|
+
ignored_output, ignored_status = Open3.capture2e("git", "ls-files", "--others", "--ignored", "--exclude-standard", chdir: root)
|
|
25
|
+
return paths unless ignored_status.success?
|
|
26
|
+
|
|
27
|
+
paths + ignored_output.lines.map(&:chomp).reject(&:empty?)
|
|
22
28
|
rescue StandardError
|
|
23
29
|
[]
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
def git_repository?(root)
|
|
33
|
+
_output, status = Open3.capture2e("git", "rev-parse", "--is-inside-work-tree", chdir: root)
|
|
34
|
+
status.success?
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
def scanned_paths(root)
|
|
27
40
|
root_path = Pathname.new(root)
|
|
28
41
|
paths = []
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../terminal_text"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Renderer for the editable composer text area.
|
|
@@ -8,7 +10,7 @@ module Kward
|
|
|
8
10
|
|
|
9
11
|
def composer_layout(width, height = screen_height)
|
|
10
12
|
return interactive_layout(width, height) if interactive_active_locked?
|
|
11
|
-
return editor_layout(width, height) if
|
|
13
|
+
return editor_layout(width, height) if editor_visible?
|
|
12
14
|
return compact_composer_layout(width) if height < 4
|
|
13
15
|
return question_composer_layout(width, height) if @question_state
|
|
14
16
|
|
|
@@ -35,45 +37,42 @@ module Kward
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def compact_composer_layout(width)
|
|
38
|
-
cursor_line, cursor_col =
|
|
40
|
+
lines, cursor_line, cursor_col = @composer.lines_and_cursor
|
|
39
41
|
prefix = "#{@prompt_label} "
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
prefix_width = TerminalText.width(prefix)
|
|
43
|
+
line = lines[cursor_line] || ""
|
|
44
|
+
input_width = [width - prefix_width, 1].max
|
|
45
|
+
layout = TerminalText.wrap(line, width: input_width, cursor: cursor_col)
|
|
46
|
+
visible = layout[:rows][layout[:cursor_row]].to_s
|
|
47
|
+
row = visible_ljust(TerminalText.truncate("#{prefix}#{visible}", width), width)
|
|
48
|
+
[[row], 0, [prefix_width + layout[:cursor_col], width - 1].min]
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def input_layout(content_width)
|
|
49
|
-
cursor_line, cursor_col =
|
|
52
|
+
lines, cursor_line, cursor_col = @composer.lines_and_cursor
|
|
50
53
|
rows = []
|
|
51
54
|
cursor_row = 0
|
|
55
|
+
cursor_col_in_row = 0
|
|
52
56
|
rendered_row_offset = 0
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
lines.each_with_index do |line, index|
|
|
55
59
|
prefix = input_prefix(index)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if index == cursor_line && cursor_col == line.length && line.length.positive? && (line.length % available).zero?
|
|
61
|
-
chunks << ""
|
|
62
|
-
end
|
|
60
|
+
prefix_width = TerminalText.width(prefix)
|
|
61
|
+
continuation_prefix = " " * prefix_width
|
|
62
|
+
available = [content_width - prefix_width, 1].max
|
|
63
|
+
layout = TerminalText.wrap(line, width: available, cursor: index == cursor_line ? cursor_col : nil)
|
|
63
64
|
|
|
64
65
|
if index == cursor_line
|
|
65
|
-
cursor_row = rendered_row_offset +
|
|
66
|
+
cursor_row = rendered_row_offset + layout[:cursor_row]
|
|
67
|
+
cursor_col_in_row = prefix_width + layout[:cursor_col]
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
layout[:rows].each_with_index do |chunk, chunk_index|
|
|
69
71
|
rows << "#{chunk_index.zero? ? prefix : continuation_prefix}#{chunk}"
|
|
70
72
|
end
|
|
71
|
-
rendered_row_offset +=
|
|
73
|
+
rendered_row_offset += layout[:rows].length
|
|
72
74
|
end
|
|
73
75
|
|
|
74
|
-
prefix = input_prefix(cursor_line)
|
|
75
|
-
available = [content_width - prefix.length, 1].max
|
|
76
|
-
cursor_col_in_row = prefix.length + (cursor_col % available)
|
|
77
76
|
[rows, cursor_row, cursor_col_in_row]
|
|
78
77
|
end
|
|
79
78
|
|
|
@@ -81,23 +80,28 @@ module Kward
|
|
|
81
80
|
title = composer_title
|
|
82
81
|
status = cached_composer_status_text
|
|
83
82
|
if status
|
|
84
|
-
gap = width - 2 - ANSI.strip(title)
|
|
83
|
+
gap = width - 2 - TerminalText.width(ANSI.strip(title)) - TerminalText.width(ANSI.strip(status))
|
|
85
84
|
if gap >= 0
|
|
86
85
|
return colored("╭", :primary_green) + title + colored("─" * gap, :primary_green) + status + colored("╮", :primary_green)
|
|
87
86
|
end
|
|
88
87
|
end
|
|
89
88
|
plain_title = ANSI.strip(title)
|
|
90
|
-
"#{colored("╭", :primary_green)}#{title}#{colored("─" * [width - plain_title
|
|
89
|
+
"#{colored("╭", :primary_green)}#{title}#{colored("─" * [width - TerminalText.width(plain_title) - 2, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def composer_title
|
|
94
93
|
label = composer_title_label
|
|
95
94
|
if @busy && @queued_count.positive?
|
|
96
|
-
status_composer_text(busy_title("#{label} · #{@queued_count} queued"))
|
|
95
|
+
status_composer_text(busy_title("#{label} · #{colored("#{@queued_count} queued", :metadata)}"))
|
|
97
96
|
elsif @busy && @steered_count.to_i.positive?
|
|
98
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} steering"))
|
|
97
|
+
status_composer_text(busy_title("#{label} · #{colored(spinner_frame, :activity, :bold)} steering"))
|
|
98
|
+
elsif @busy && response_arriving?
|
|
99
|
+
status_composer_text(busy_title("#{label} · #{colored("✦", :activity, :bold)} responding"))
|
|
99
100
|
elsif @busy
|
|
100
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} #{@busy_activity}"))
|
|
101
|
+
status_composer_text(busy_title("#{label} · #{colored(spinner_frame, :activity, :bold)} #{@busy_activity}"))
|
|
102
|
+
elsif @completion_status
|
|
103
|
+
completion = colored(@completion_status[:text], @completion_status[:style], :bold)
|
|
104
|
+
status_composer_text("#{label} · #{completion}")
|
|
101
105
|
else
|
|
102
106
|
status_composer_text(label)
|
|
103
107
|
end
|
|
@@ -110,6 +114,8 @@ module Kward
|
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
def busy_title(text)
|
|
117
|
+
elapsed = busy_elapsed_text
|
|
118
|
+
text = "#{text} · #{colored(elapsed, :metadata)}" if elapsed
|
|
113
119
|
@busy_help ? "#{text} · #{BUSY_HELP_TEXT}" : text
|
|
114
120
|
end
|
|
115
121
|
|
|
@@ -140,7 +146,7 @@ module Kward
|
|
|
140
146
|
label_left = 4
|
|
141
147
|
@tabs.each_with_index.map do |label, index|
|
|
142
148
|
text = tab_label(label, index)
|
|
143
|
-
width = ANSI.strip(text)
|
|
149
|
+
width = TerminalText.width(ANSI.strip(text))
|
|
144
150
|
slot = {
|
|
145
151
|
left: label_left - 2,
|
|
146
152
|
label_left: label_left,
|
|
@@ -185,18 +191,25 @@ module Kward
|
|
|
185
191
|
|
|
186
192
|
visible_offset = 0
|
|
187
193
|
last_index = nil
|
|
188
|
-
text.to_s.scan(/\e\[[0-9;:]*m
|
|
194
|
+
text.to_s.scan(/\e\[[0-9;:]*m|\X/m).each do |part|
|
|
189
195
|
if part.start_with?("\e")
|
|
190
196
|
index = visible_offset.positive? ? last_index : left
|
|
191
197
|
row[index] = row[index].to_s + part if index&.between?(0, row.length - 1)
|
|
192
198
|
next
|
|
193
199
|
end
|
|
194
200
|
|
|
201
|
+
part_width = TerminalText.width(part)
|
|
202
|
+
if part_width.zero?
|
|
203
|
+
row[last_index] = row[last_index].to_s + part if last_index
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
|
|
195
207
|
index = left + visible_offset
|
|
196
208
|
break if index >= row.length
|
|
197
209
|
row[index] = row[index].to_s.sub(/\A /, "") + part unless index.negative?
|
|
210
|
+
1.upto(part_width - 1) { |offset| row[index + offset] = "" if (index + offset).between?(0, row.length - 1) }
|
|
198
211
|
last_index = index
|
|
199
|
-
visible_offset +=
|
|
212
|
+
visible_offset += part_width
|
|
200
213
|
end
|
|
201
214
|
end
|
|
202
215
|
|
|
@@ -252,19 +265,10 @@ module Kward
|
|
|
252
265
|
[[input_cap, height - 3 - overlay_count - footer_count - attachment_count].min, 1].max
|
|
253
266
|
end
|
|
254
267
|
|
|
255
|
-
def input_lines
|
|
256
|
-
lines = composer_input.split("\n", -1)
|
|
257
|
-
lines.empty? ? [""] : lines
|
|
258
|
-
end
|
|
259
|
-
|
|
260
268
|
def input_prefix(_index)
|
|
261
269
|
""
|
|
262
270
|
end
|
|
263
271
|
|
|
264
|
-
def cursor_logical_position
|
|
265
|
-
@composer.cursor_logical_position
|
|
266
|
-
end
|
|
267
|
-
|
|
268
272
|
def image_viewer_preview_rows(height, width: screen_width)
|
|
269
273
|
available_rows = max_project_browser_rows(height, width: width)
|
|
270
274
|
desired_rows = [(IMAGE_VIEWER_MAX_ROWS * image_viewer_zoom).round, 1].max
|
|
@@ -276,7 +280,7 @@ module Kward
|
|
|
276
280
|
preview_rows = image_viewer_preview_rows(height, width: width)
|
|
277
281
|
title = visible_truncate(" Image: #{@image_viewer_state[:display_path]} ", content_width)
|
|
278
282
|
rows = [
|
|
279
|
-
colored("╭", :primary_green) + title + colored("─" * [width - ANSI.strip(title)
|
|
283
|
+
colored("╭", :primary_green) + title + colored("─" * [width - TerminalText.width(ANSI.strip(title)) - 2, 0].max, :primary_green) + colored("╮", :primary_green),
|
|
280
284
|
image_viewer_graphic_row(width, image_viewer_sequence(width, height))
|
|
281
285
|
]
|
|
282
286
|
rows.concat(Array.new(preview_rows - 1) { box_content_row("", content_width) })
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative "../text_boundary"
|
|
2
|
+
require_relative "../text_matcher"
|
|
3
|
+
require_relative "../terminal_text"
|
|
2
4
|
|
|
3
5
|
# Namespace for the Kward CLI agent runtime.
|
|
4
6
|
module Kward
|
|
@@ -41,6 +43,8 @@ module Kward
|
|
|
41
43
|
@history_search_query = nil
|
|
42
44
|
@history_search_draft = nil
|
|
43
45
|
@history_search_index = 0
|
|
46
|
+
@history_search_matches_query = nil
|
|
47
|
+
@history_search_matches = nil
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# Removes all pending attachments without changing text input.
|
|
@@ -76,31 +80,33 @@ module Kward
|
|
|
76
80
|
@cursor += string.length
|
|
77
81
|
end
|
|
78
82
|
|
|
79
|
-
# Deletes one
|
|
83
|
+
# Deletes one grapheme before the cursor.
|
|
80
84
|
def delete_before_cursor
|
|
81
85
|
return false if @cursor.zero?
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
@
|
|
87
|
+
previous = TerminalText.previous_grapheme_boundary(@input, @cursor)
|
|
88
|
+
@input = @input[0...previous] + @input[@cursor..]
|
|
89
|
+
@cursor = previous
|
|
85
90
|
true
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
# Deletes one
|
|
93
|
+
# Deletes one grapheme at the cursor without moving it.
|
|
89
94
|
def delete_at_cursor
|
|
90
95
|
return false unless @cursor < @input.length
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
following = TerminalText.next_grapheme_boundary(@input, @cursor)
|
|
98
|
+
@input = @input[0...@cursor] + @input[following..]
|
|
93
99
|
true
|
|
94
100
|
end
|
|
95
101
|
|
|
96
|
-
# Moves the cursor one
|
|
102
|
+
# Moves the cursor one grapheme left when possible.
|
|
97
103
|
def move_cursor_left
|
|
98
|
-
@cursor
|
|
104
|
+
@cursor = TerminalText.previous_grapheme_boundary(@input, @cursor) if @cursor.positive?
|
|
99
105
|
end
|
|
100
106
|
|
|
101
|
-
# Moves the cursor one
|
|
107
|
+
# Moves the cursor one grapheme right when possible.
|
|
102
108
|
def move_cursor_right
|
|
103
|
-
@cursor
|
|
109
|
+
@cursor = TerminalText.next_grapheme_boundary(@input, @cursor) if @cursor < @input.length
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
# Moves the cursor to the beginning of the input buffer.
|
|
@@ -164,10 +170,14 @@ module Kward
|
|
|
164
170
|
@cursor = @input.length
|
|
165
171
|
end
|
|
166
172
|
|
|
167
|
-
# Returns `[row, column]` for cursor
|
|
168
|
-
def
|
|
173
|
+
# Returns `[lines, row, column]` for cursor-aware multi-line layout.
|
|
174
|
+
def lines_and_cursor
|
|
175
|
+
lines = @input.split("\n", -1)
|
|
169
176
|
before_cursor = @input[0...@cursor]
|
|
170
|
-
|
|
177
|
+
row = before_cursor.count("\n")
|
|
178
|
+
line_start = before_cursor.rindex("\n")
|
|
179
|
+
column = line_start ? before_cursor.length - line_start - 1 : before_cursor.length
|
|
180
|
+
[lines.empty? ? [""] : lines, row, column]
|
|
171
181
|
end
|
|
172
182
|
|
|
173
183
|
# Replaces the in-memory history list with persisted entries.
|
|
@@ -175,6 +185,7 @@ module Kward
|
|
|
175
185
|
@history = Array(values).map(&:to_s).reject { |value| value.strip.empty? }
|
|
176
186
|
reset_history_navigation
|
|
177
187
|
reset_history_search
|
|
188
|
+
invalidate_history_search_matches
|
|
178
189
|
end
|
|
179
190
|
|
|
180
191
|
# Stores a submitted input unless it is blank or duplicates the previous entry.
|
|
@@ -184,6 +195,7 @@ module Kward
|
|
|
184
195
|
return false if @history.last == value
|
|
185
196
|
|
|
186
197
|
@history << value
|
|
198
|
+
invalidate_history_search_matches
|
|
187
199
|
true
|
|
188
200
|
end
|
|
189
201
|
|
|
@@ -234,9 +246,13 @@ module Kward
|
|
|
234
246
|
|
|
235
247
|
def history_search_matches
|
|
236
248
|
query = @history_search_query.to_s.downcase
|
|
237
|
-
return @
|
|
249
|
+
return @history_search_matches if @history_search_matches_query == query && @history_search_matches
|
|
238
250
|
|
|
239
|
-
|
|
251
|
+
pattern = TextMatcher.subsequence_pattern(query)
|
|
252
|
+
@history_search_matches_query = query
|
|
253
|
+
@history_search_matches = @history.reverse.select do |value|
|
|
254
|
+
TextMatcher.subsequence?(value.downcase, query, pattern)
|
|
255
|
+
end
|
|
240
256
|
end
|
|
241
257
|
|
|
242
258
|
def selected_history_search_match
|
|
@@ -274,16 +290,9 @@ module Kward
|
|
|
274
290
|
@history_search_index = 0
|
|
275
291
|
end
|
|
276
292
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if index
|
|
281
|
-
value = value[(index + 1)..].to_s
|
|
282
|
-
true
|
|
283
|
-
else
|
|
284
|
-
false
|
|
285
|
-
end
|
|
286
|
-
end
|
|
293
|
+
def invalidate_history_search_matches
|
|
294
|
+
@history_search_matches_query = nil
|
|
295
|
+
@history_search_matches = nil
|
|
287
296
|
end
|
|
288
297
|
end
|
|
289
298
|
end
|
|
@@ -83,9 +83,12 @@ module Kward
|
|
|
83
83
|
|
|
84
84
|
def editor_insert_tab
|
|
85
85
|
if @editor_state.multi_cursor? || @editor_state.selection_ranges.any?
|
|
86
|
+
reset_editor_word_completion
|
|
86
87
|
return @editor_state.replace_selections(editor_indent_unit)
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
return true if editor_complete_buffer_word
|
|
91
|
+
|
|
89
92
|
if current_editor_auto_indent? && editor_cursor_in_leading_indent?
|
|
90
93
|
return editor_smart_tab_forward
|
|
91
94
|
end
|
|
@@ -142,7 +145,7 @@ module Kward
|
|
|
142
145
|
line = @editor_state.lines[line_index].to_s
|
|
143
146
|
before_cursor = line[0...column].to_s
|
|
144
147
|
base_indent = line[/\A[ \t]*/].to_s
|
|
145
|
-
language =
|
|
148
|
+
language = editor_effective_syntax_language(line_index)
|
|
146
149
|
indent = base_indent.dup
|
|
147
150
|
indent += editor_indent_unit if editor_line_opens_indent?(before_cursor, language)
|
|
148
151
|
indent
|
|
@@ -176,7 +179,8 @@ module Kward
|
|
|
176
179
|
|
|
177
180
|
def editor_expected_indent_for_line(line_index)
|
|
178
181
|
line = @editor_state.lines[line_index].to_s
|
|
179
|
-
|
|
182
|
+
language = editor_effective_syntax_language(line_index)
|
|
183
|
+
code = editor_indent_code(line, language).strip
|
|
180
184
|
matching_indent = editor_matching_indent_for_line(code, line_index)
|
|
181
185
|
return matching_indent if matching_indent
|
|
182
186
|
|
|
@@ -184,7 +188,7 @@ module Kward
|
|
|
184
188
|
return "" unless previous_line
|
|
185
189
|
|
|
186
190
|
indent = previous_line[:indent].dup
|
|
187
|
-
indent += editor_indent_unit if editor_line_opens_indent?(previous_line[:code],
|
|
191
|
+
indent += editor_indent_unit if editor_line_opens_indent?(previous_line[:code], previous_line[:language])
|
|
188
192
|
indent
|
|
189
193
|
end
|
|
190
194
|
|
|
@@ -192,18 +196,12 @@ module Kward
|
|
|
192
196
|
return nil if code.empty?
|
|
193
197
|
return editor_matching_punctuation_indent(code[0], line_index) if editor_closing_punctuation?(code[0])
|
|
194
198
|
|
|
195
|
-
|
|
199
|
+
language = editor_effective_syntax_language(line_index)
|
|
200
|
+
editor_matching_word_indent(line_index) if editor_completed_word_closer?(code, language)
|
|
196
201
|
end
|
|
197
202
|
|
|
198
203
|
def previous_non_blank_editor_line(line_index)
|
|
199
|
-
(line_index
|
|
200
|
-
line = @editor_state.lines[index].to_s
|
|
201
|
-
code = editor_indent_code(line, editor_syntax_language).rstrip
|
|
202
|
-
next if code.strip.empty?
|
|
203
|
-
|
|
204
|
-
return { indent: line[/\A[ \t]*/].to_s, code: code }
|
|
205
|
-
end
|
|
206
|
-
nil
|
|
204
|
+
editor_previous_code_lines(line_index).last
|
|
207
205
|
end
|
|
208
206
|
|
|
209
207
|
def editor_update_current_line_indent(indent, preserve_content_column: false)
|
|
@@ -245,7 +243,7 @@ module Kward
|
|
|
245
243
|
line = @editor_state.lines[line_index].to_s
|
|
246
244
|
before_cursor = line[0...column].to_s
|
|
247
245
|
base_indent = line[/\A[ \t]*/].to_s
|
|
248
|
-
language =
|
|
246
|
+
language = editor_effective_syntax_language(line_index)
|
|
249
247
|
opens_indent = editor_line_opens_indent?(before_cursor, language)
|
|
250
248
|
paired_closer = editor_next_paired_closer
|
|
251
249
|
return nil unless paired_closer
|
|
@@ -407,7 +405,8 @@ module Kward
|
|
|
407
405
|
end
|
|
408
406
|
|
|
409
407
|
def editor_closing_punctuation?(text)
|
|
410
|
-
|
|
408
|
+
line_index = @editor_state.cursor_line_and_column.first
|
|
409
|
+
PUNCTUATION_PAIRS.key?(text) && PUNCTUATION_INDENT_LANGUAGES.include?(editor_effective_syntax_language(line_index))
|
|
411
410
|
end
|
|
412
411
|
|
|
413
412
|
def editor_reindent_for_closing_punctuation(text)
|
|
@@ -422,7 +421,7 @@ module Kward
|
|
|
422
421
|
line_index, column = @editor_state.cursor_line_and_column
|
|
423
422
|
line = @editor_state.lines[line_index].to_s
|
|
424
423
|
before_cursor = line[0...column].to_s
|
|
425
|
-
return unless editor_completed_word_closer?(before_cursor,
|
|
424
|
+
return unless editor_completed_word_closer?(before_cursor, editor_effective_syntax_language(line_index))
|
|
426
425
|
|
|
427
426
|
indent = editor_matching_word_indent(line_index)
|
|
428
427
|
editor_reindent_current_line(indent) if indent
|
|
@@ -474,7 +473,8 @@ module Kward
|
|
|
474
473
|
end
|
|
475
474
|
|
|
476
475
|
def editor_matching_word_indent(line_index = nil)
|
|
477
|
-
|
|
476
|
+
line_index ||= @editor_state.cursor_line_and_column.first
|
|
477
|
+
case editor_effective_syntax_language(line_index)
|
|
478
478
|
when :ruby
|
|
479
479
|
editor_matching_keyword_indent("end", %w[end], line_index)
|
|
480
480
|
when :lua
|
|
@@ -510,7 +510,7 @@ module Kward
|
|
|
510
510
|
next if code.empty?
|
|
511
511
|
|
|
512
512
|
stack.pop if editor_word_closer_line?(code, closers)
|
|
513
|
-
stack << line[:indent] if editor_word_opener_line?(code, opener)
|
|
513
|
+
stack << line[:indent] if editor_word_opener_line?(code, opener, line[:language])
|
|
514
514
|
end
|
|
515
515
|
stack.last || ""
|
|
516
516
|
end
|
|
@@ -529,11 +529,16 @@ module Kward
|
|
|
529
529
|
|
|
530
530
|
def editor_previous_code_lines(line_index = nil)
|
|
531
531
|
line_index ||= @editor_state.cursor_line_and_column.first
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
language = editor_effective_syntax_language(line_index)
|
|
533
|
+
first_index = editor_syntax_context_start(line_index)
|
|
534
|
+
first_index = 0 unless first_index && language != :markdown
|
|
535
|
+
|
|
536
|
+
(first_index...line_index).filter_map do |index|
|
|
537
|
+
line = @editor_state.lines[index].to_s
|
|
538
|
+
code = editor_indent_code(line, language).rstrip
|
|
534
539
|
next if code.strip.empty?
|
|
535
540
|
|
|
536
|
-
{ indent: line[/\A[ \t]*/].to_s, code: code }
|
|
541
|
+
{ indent: line[/\A[ \t]*/].to_s, code: code, language: language }
|
|
537
542
|
end
|
|
538
543
|
end
|
|
539
544
|
|
|
@@ -541,8 +546,8 @@ module Kward
|
|
|
541
546
|
code.to_s.scan(/[{}\[\]()]/)
|
|
542
547
|
end
|
|
543
548
|
|
|
544
|
-
def editor_word_opener_line?(code, opener)
|
|
545
|
-
case
|
|
549
|
+
def editor_word_opener_line?(code, opener, language)
|
|
550
|
+
case language
|
|
546
551
|
when :ruby
|
|
547
552
|
editor_ruby_line_opens_indent?(code)
|
|
548
553
|
when :lua
|