canopus 0.4.0 → 0.5.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 +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -9,9 +9,11 @@ module Canopus
|
|
|
9
9
|
Link = Data.define(:kind, :target, :line, :column)
|
|
10
10
|
|
|
11
11
|
def terminal_link_at(point)
|
|
12
|
-
return unless @terminal_bounds&.contains?(point) &&
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
return unless @terminal_bounds&.contains?(point) && displayed_terminal
|
|
13
|
+
position = terminal_source_point(point)
|
|
14
|
+
return unless position
|
|
15
|
+
column, row = position
|
|
16
|
+
cells = terminal_row(row)
|
|
15
17
|
column -= 1 while column.positive? && cells[column].width.zero?
|
|
16
18
|
cell = cells[column]
|
|
17
19
|
return terminal_link(cell.hyperlink, explicit: true) if cell.hyperlink
|
|
@@ -63,24 +65,34 @@ module Canopus
|
|
|
63
65
|
format("#%02x%02x%02x", ramp[index / 36], ramp[index / 6 % 6], ramp[index % 6])
|
|
64
66
|
end
|
|
65
67
|
def terminal_point(point)
|
|
66
|
-
grid =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
grid = displayed_terminal.grid
|
|
69
|
+
position = terminal_source_point(point)
|
|
70
|
+
return unless position
|
|
71
|
+
column, row = position
|
|
72
|
+
[column, (row - grid.scrollback.length).clamp(0, grid.rows - 1)]
|
|
70
73
|
end
|
|
71
74
|
def terminal_scroll(delta)
|
|
72
|
-
|
|
75
|
+
map = terminal_row_map
|
|
76
|
+
maximum = [map[:visible] - displayed_terminal.grid.rows, 0].max
|
|
77
|
+
@terminal_scroll = ((@terminal_scroll || 0) - delta).clamp(0, maximum)
|
|
73
78
|
@terminal_selection = nil
|
|
74
79
|
end
|
|
75
80
|
def terminal_select(point, extend: false)
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
position = terminal_source_point(point)
|
|
82
|
+
return unless position
|
|
83
|
+
column, row = position
|
|
84
|
+
index = [row, column]
|
|
78
85
|
@terminal_selection = extend && @terminal_selection ? [@terminal_selection.first, index] : [index, index]
|
|
79
86
|
end
|
|
80
87
|
def terminal_selected_text
|
|
81
88
|
return "" unless @terminal_selection
|
|
82
89
|
first, last = @terminal_selection.sort
|
|
83
|
-
|
|
90
|
+
map = terminal_row_map
|
|
91
|
+
from_row = terminal_display_for_source(map, first.first)
|
|
92
|
+
to_row = terminal_display_for_source(map, last.first)
|
|
93
|
+
return "" unless from_row && to_row
|
|
94
|
+
(from_row..to_row).map do |display_row|
|
|
95
|
+
row = terminal_source_for_display(map, display_row)
|
|
84
96
|
cells = terminal_row(row)
|
|
85
97
|
from = row == first.first ? first.last : 0
|
|
86
98
|
to = row == last.first ? last.last : cells.length - 1
|
|
@@ -88,7 +100,150 @@ module Canopus
|
|
|
88
100
|
end.join("\n")
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
def terminal_command(direction)
|
|
104
|
+
terminal = displayed_terminal
|
|
105
|
+
return false unless terminal&.respond_to?(:commands) && !terminal.grid.alternate?
|
|
106
|
+
map = terminal_row_map(terminal)
|
|
107
|
+
maximum = [map[:visible] - terminal.grid.rows, 0].max
|
|
108
|
+
first_display = maximum - (@terminal_scroll || 0).floor
|
|
109
|
+
current = map[:base] + terminal_source_for_display(map, first_display)
|
|
110
|
+
commands = terminal.commands.select { |command| command.prompt_row >= map[:base] }
|
|
111
|
+
command = if direction == :previous
|
|
112
|
+
commands.reverse.find { |item| @terminal_scroll.to_i.zero? ? item.prompt_row <= current : item.prompt_row < current }
|
|
113
|
+
elsif direction == :next
|
|
114
|
+
commands.find { |item| item.prompt_row > current }
|
|
115
|
+
else
|
|
116
|
+
raise ArgumentError, "invalid terminal command direction"
|
|
117
|
+
end
|
|
118
|
+
terminal_command_jump(command)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def terminal_command_jump(command)
|
|
122
|
+
return false unless command && displayed_terminal && !displayed_terminal.grid.alternate?
|
|
123
|
+
map = terminal_row_map
|
|
124
|
+
source = command.prompt_row - map[:base]
|
|
125
|
+
display = terminal_display_for_source(map, source)
|
|
126
|
+
return false unless display
|
|
127
|
+
maximum = [map[:visible] - displayed_terminal.grid.rows, 0].max
|
|
128
|
+
@terminal_scroll = maximum - [display, maximum].min
|
|
129
|
+
@terminal_selection = nil
|
|
130
|
+
true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def terminal_toggle_command(command = nil)
|
|
134
|
+
terminal = displayed_terminal
|
|
135
|
+
return false unless terminal&.respond_to?(:commands) && !terminal.grid.alternate?
|
|
136
|
+
map = terminal_row_map(terminal)
|
|
137
|
+
unless command
|
|
138
|
+
maximum = [map[:visible] - terminal.grid.rows, 0].max
|
|
139
|
+
first = maximum - (@terminal_scroll || 0).floor
|
|
140
|
+
last = [first + terminal.grid.rows - 1, map[:visible] - 1].min
|
|
141
|
+
current = map[:base] + terminal_source_for_display(map, last)
|
|
142
|
+
command = terminal.commands.reverse.find { |item| item.prompt_row <= current && terminal_fold_interval(item, map) }
|
|
143
|
+
end
|
|
144
|
+
return false unless command && terminal_fold_interval(command, map)
|
|
145
|
+
folds = terminal_folds(terminal)
|
|
146
|
+
folds[command.id] ? folds.delete(command.id) : folds[command.id] = true
|
|
147
|
+
@terminal_selection = nil
|
|
148
|
+
terminal_command_jump(command)
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def focus_terminal(terminal)
|
|
153
|
+
return terminal if @terminal_owner.equal?(terminal)
|
|
154
|
+
store_terminal_state if @terminal_owner
|
|
155
|
+
@terminal_owner = terminal
|
|
156
|
+
state = terminal&.instance_variable_get(:@canopus_terminal_view_state)
|
|
157
|
+
@terminal_scroll = state&.fetch(:scroll, 0) || 0
|
|
158
|
+
@terminal_selection = state&.[](:selection)
|
|
159
|
+
@terminal_bounds = state&.[](:bounds)
|
|
160
|
+
@terminal_visible_rows = state&.[](:visible_rows)
|
|
161
|
+
@terminal_first = state&.fetch(:first, 0) || 0
|
|
162
|
+
@terminal_first_display = state&.fetch(:first_display, 0) || 0
|
|
163
|
+
@terminal_cell_width = state&.[](:cell_width)
|
|
164
|
+
@terminal_line_height = state&.[](:line_height)
|
|
165
|
+
@terminal_font_size = state&.[](:font_size)
|
|
166
|
+
terminal
|
|
167
|
+
end
|
|
168
|
+
|
|
91
169
|
private
|
|
170
|
+
def store_terminal_state
|
|
171
|
+
@terminal_owner.instance_variable_set(:@canopus_terminal_view_state,
|
|
172
|
+
{scroll: @terminal_scroll || 0, selection: @terminal_selection,
|
|
173
|
+
bounds: @terminal_bounds, visible_rows: @terminal_visible_rows, first: @terminal_first || 0,
|
|
174
|
+
first_display: @terminal_first_display || 0, cell_width: @terminal_cell_width,
|
|
175
|
+
line_height: @terminal_line_height, font_size: @terminal_font_size})
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def terminal_visual_point(point)
|
|
179
|
+
grid = displayed_terminal.grid
|
|
180
|
+
column = ((point.x - @terminal_bounds.x) / @terminal_cell_width).floor.clamp(0, grid.columns - 1)
|
|
181
|
+
row = ((point.y - @terminal_bounds.y) / (@terminal_line_height || @line_height)).floor.clamp(0, grid.rows - 1)
|
|
182
|
+
[column, row]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def terminal_source_point(point)
|
|
186
|
+
column, row = terminal_visual_point(point)
|
|
187
|
+
source = @terminal_visible_rows ? @terminal_visible_rows[row] : @terminal_first.to_i + row
|
|
188
|
+
[column, source] if source
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def terminal_folds(terminal)
|
|
192
|
+
terminal.instance_variable_get(:@canopus_command_folds) ||
|
|
193
|
+
terminal.instance_variable_set(:@canopus_command_folds, {})
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def terminal_fold_interval(command, map)
|
|
197
|
+
range = command.output_range
|
|
198
|
+
return unless range && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
199
|
+
first = [range.begin, command.prompt_row + 1, map[:base]].max - map[:base]
|
|
200
|
+
last = [range.end - 1, map[:base] + map[:length] - 1].min - map[:base]
|
|
201
|
+
[first, last] if first <= last
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def terminal_row_map(terminal = displayed_terminal)
|
|
205
|
+
grid = terminal.grid
|
|
206
|
+
base = grid.scrollback.total - grid.scrollback.length
|
|
207
|
+
length = grid.scrollback.length + grid.rows
|
|
208
|
+
map = {base: base, length: length}
|
|
209
|
+
return map.merge(intervals: [], visible: length) if grid.alternate?
|
|
210
|
+
commands = terminal.respond_to?(:commands) ? terminal.commands : []
|
|
211
|
+
commands = commands.select { |command| command.prompt_row.between?(base, base + length - 1) }
|
|
212
|
+
folds = terminal_folds(terminal)
|
|
213
|
+
ids = commands.to_h { |command| [command.id, true] }
|
|
214
|
+
folds.delete_if { |id| !ids.key?(id) }
|
|
215
|
+
intervals = commands.filter_map { |command| terminal_fold_interval(command, map) if folds[command.id] }
|
|
216
|
+
.sort_by(&:first).each_with_object([]) do |interval, merged|
|
|
217
|
+
if merged.last && interval.first <= merged.last.last + 1
|
|
218
|
+
merged.last[1] = [merged.last.last, interval.last].max
|
|
219
|
+
else
|
|
220
|
+
merged << interval
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
map.merge(intervals: intervals, visible: length - intervals.sum { |first, last| last - first + 1 })
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def terminal_source_for_display(map, display)
|
|
227
|
+
return unless display.between?(0, map[:visible] - 1)
|
|
228
|
+
source = display
|
|
229
|
+
map[:intervals].each do |first, last|
|
|
230
|
+
break if source < first
|
|
231
|
+
source += last - first + 1
|
|
232
|
+
end
|
|
233
|
+
source
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def terminal_display_for_source(map, source)
|
|
237
|
+
return unless source.between?(0, map[:length] - 1)
|
|
238
|
+
hidden = 0
|
|
239
|
+
map[:intervals].each do |first, last|
|
|
240
|
+
return if source.between?(first, last)
|
|
241
|
+
break if source < first
|
|
242
|
+
hidden += last - first + 1
|
|
243
|
+
end
|
|
244
|
+
source - hidden
|
|
245
|
+
end
|
|
246
|
+
|
|
92
247
|
def terminal_link(value, explicit: false)
|
|
93
248
|
return if value.empty? || value.bytesize > 8192 || value.match?(/[\x00-\x1f\x7f]/)
|
|
94
249
|
if value.match?(/\Ahttps?:\/\//i)
|
|
@@ -113,7 +268,7 @@ module Canopus
|
|
|
113
268
|
if (location = value.match(/\A(.+?)(?::(\d+)(?::(\d+))?|\((\d+)(?:,(\d+))?\))\z/))
|
|
114
269
|
value, line, column = location[1], (location[2] || location[4]).to_i, (location[3] || location[5])&.to_i
|
|
115
270
|
end
|
|
116
|
-
directory =
|
|
271
|
+
directory = displayed_terminal.respond_to?(:cwd) ? displayed_terminal.cwd : displayed_terminal.vt.cwd
|
|
117
272
|
directory = @workspace.root unless directory && File.directory?(directory)
|
|
118
273
|
path = File.expand_path(value, directory)
|
|
119
274
|
Link.new(:file, path, line, column) if File.file?(path)
|
|
@@ -131,47 +286,113 @@ module Canopus
|
|
|
131
286
|
end
|
|
132
287
|
|
|
133
288
|
def terminal_row(index)
|
|
134
|
-
grid =
|
|
289
|
+
grid = displayed_terminal.grid
|
|
135
290
|
index < grid.scrollback.length ? grid.scrollback[index] : grid.cells[index - grid.scrollback.length]
|
|
136
291
|
end
|
|
292
|
+
def displayed_terminal = @terminal_owner || @workspace.terminal
|
|
137
293
|
def render_terminal(bounds)
|
|
138
294
|
fill(bounds, :panel)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
295
|
+
active = @workspace.terminal
|
|
296
|
+
unless active
|
|
297
|
+
focus_terminal(nil)
|
|
142
298
|
text("No terminals — Ctrl+Shift+` to create one", bounds.x + 12, bounds.y + 8, color: :muted, size: 12)
|
|
143
299
|
return
|
|
144
300
|
end
|
|
145
|
-
|
|
146
|
-
|
|
301
|
+
render_pty_tabs(bounds, entries: @workspace.terminals, active: active,
|
|
302
|
+
title: ->(item) { @workspace.terminal_title(item) }, prefix: :terminal)
|
|
303
|
+
body = Zaniah::Bounds.new(bounds.x, bounds.y + 28, bounds.width, [bounds.height - 28, 0].max)
|
|
304
|
+
render_terminal_layout(@workspace.terminal_layout || {terminal: active}, body)
|
|
305
|
+
focus_terminal(active)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def render_task_output(bounds)
|
|
309
|
+
render_pty_panel(bounds, entries: @workspace.task_outputs, active: @workspace.task_output,
|
|
310
|
+
title: ->(item) { @workspace.task_output_title(item) }, prefix: :task_output,
|
|
311
|
+
empty: "No task output — run task with Cmd/Ctrl+Shift+B") { |item| item.terminal }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def render_pty_panel(bounds, entries:, active:, title:, prefix:, empty:, &terminal_for)
|
|
315
|
+
fill(bounds, :panel)
|
|
316
|
+
terminal = active && terminal_for.call(active)
|
|
317
|
+
unless terminal
|
|
318
|
+
focus_terminal(nil)
|
|
319
|
+
text(empty, bounds.x + 12, bounds.y + 8, color: :muted, size: 12)
|
|
320
|
+
return
|
|
147
321
|
end
|
|
322
|
+
render_pty_tabs(bounds, entries: entries, active: active, title: title, prefix: prefix)
|
|
323
|
+
focus_terminal(terminal)
|
|
324
|
+
body = Zaniah::Bounds.new(bounds.x, bounds.y + 28, bounds.width, [bounds.height - 28, 0].max)
|
|
325
|
+
render_terminal_content(terminal, body, prefix)
|
|
326
|
+
store_terminal_state
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def render_pty_tabs(bounds, entries:, active:, title:, prefix:)
|
|
148
330
|
x = bounds.x
|
|
149
|
-
|
|
150
|
-
label =
|
|
331
|
+
entries.each_with_index do |current, index|
|
|
332
|
+
label = title.call(current)
|
|
151
333
|
width = [[label.length * 7.5 + 42, 100].max, 220].min
|
|
152
334
|
tab = Zaniah::Bounds.new(x, bounds.y, width, 28)
|
|
153
|
-
fill(tab, :active_tab) if current.equal?(
|
|
154
|
-
text(label, tab.x + 10, tab.y + 6, color: current.equal?(
|
|
155
|
-
|
|
335
|
+
fill(tab, :active_tab) if current.equal?(active)
|
|
336
|
+
text(label, tab.x + 10, tab.y + 6, color: current.equal?(active) ? :foreground : :muted, size: 12)
|
|
337
|
+
tab_action = prefix == :terminal ? :terminal_tab : :task_output_tab
|
|
338
|
+
region(tab, role: :tab, label: label, action: [tab_action, index])
|
|
156
339
|
close = Zaniah::Bounds.new(tab.right - 25, tab.y, 25, tab.height)
|
|
157
|
-
|
|
158
|
-
|
|
340
|
+
running = prefix == :task_output && @workspace.task_runner.running?(current)
|
|
341
|
+
text(running ? "■" : "×", close.x + 6, close.y + 5, color: :muted, size: 12)
|
|
342
|
+
close_action = prefix == :terminal ? :terminal_close : :task_output_close
|
|
343
|
+
region(close, role: :button, label: "#{running ? 'Stop' : 'Close'} #{label}", action: [close_action, index])
|
|
159
344
|
x += width
|
|
160
345
|
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def render_terminal_layout(node, bounds)
|
|
349
|
+
if node[:terminal]
|
|
350
|
+
focus_terminal(node[:terminal])
|
|
351
|
+
render_terminal_content(node[:terminal], bounds, :terminal)
|
|
352
|
+
store_terminal_state
|
|
353
|
+
return
|
|
354
|
+
end
|
|
355
|
+
horizontal = node[:direction] == :horizontal
|
|
356
|
+
ratio = node.fetch(:ratio, 0.5)
|
|
357
|
+
node[:children].each_with_index do |child, index|
|
|
358
|
+
start, fraction = index.zero? ? [0, ratio] : [ratio, 1 - ratio]
|
|
359
|
+
part = if horizontal
|
|
360
|
+
Zaniah::Bounds.new(bounds.x + bounds.width * start, bounds.y, bounds.width * fraction, bounds.height)
|
|
361
|
+
else
|
|
362
|
+
Zaniah::Bounds.new(bounds.x, bounds.y + bounds.height * start, bounds.width, bounds.height * fraction)
|
|
363
|
+
end
|
|
364
|
+
render_terminal_layout(child, part)
|
|
365
|
+
end
|
|
366
|
+
divider = horizontal ?
|
|
367
|
+
Zaniah::Bounds.new(bounds.x + bounds.width * ratio - 2, bounds.y, 4, bounds.height) :
|
|
368
|
+
Zaniah::Bounds.new(bounds.x, bounds.y + bounds.height * ratio - 2, bounds.width, 4)
|
|
369
|
+
fill(divider, :border)
|
|
370
|
+
region(divider, role: :separator, label: "Resize terminal split", action: [:split_resize, node, bounds])
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def render_terminal_content(terminal, bounds, prefix)
|
|
161
374
|
@terminal_font_size = @workspace.settings["terminal"]["font_size"] || @font_size
|
|
162
375
|
@terminal_line_height = (@terminal_font_size * @workspace.settings["terminal"]["line_height"]).ceil
|
|
163
376
|
@terminal_cell_width = @cx.text_system&.layout_line("M", size: @terminal_font_size)&.width || @terminal_font_size * 0.6
|
|
164
377
|
columns = [(bounds.width - 24) / @terminal_cell_width, @workspace.settings["terminal"]["min_cols"]].max.floor
|
|
165
|
-
rows = [(bounds.height -
|
|
166
|
-
@workspace.resize_terminal(columns, rows)
|
|
378
|
+
rows = [(bounds.height - 12) / @terminal_line_height, @workspace.settings["terminal"]["min_rows"]].max.floor
|
|
379
|
+
prefix == :terminal ? @workspace.resize_terminal(columns, rows, terminal: terminal) : @workspace.resize_task_output(columns, rows)
|
|
167
380
|
grid = terminal.grid
|
|
168
|
-
@terminal_bounds = Zaniah::Bounds.new(bounds.x + 12, bounds.y +
|
|
169
|
-
|
|
170
|
-
|
|
381
|
+
@terminal_bounds = Zaniah::Bounds.new(bounds.x + 12, bounds.y + 4,
|
|
382
|
+
columns * @terminal_cell_width, rows * @terminal_line_height).intersect(bounds)
|
|
383
|
+
map = terminal_row_map(terminal)
|
|
384
|
+
maximum = [map[:visible] - rows, 0].max
|
|
385
|
+
@terminal_scroll = (@terminal_scroll || 0).clamp(0, maximum)
|
|
386
|
+
@terminal_first_display = maximum - @terminal_scroll.floor
|
|
387
|
+
@terminal_visible_rows = rows.times.map { |row| terminal_source_for_display(map, @terminal_first_display + row) }
|
|
388
|
+
@terminal_first = @terminal_visible_rows.compact.first || 0
|
|
171
389
|
selection = @terminal_selection&.sort
|
|
390
|
+
command_regions = []
|
|
172
391
|
@scene.clip(@terminal_bounds) do
|
|
173
392
|
rows.times do |row|
|
|
174
|
-
|
|
393
|
+
source_row = @terminal_visible_rows[row]
|
|
394
|
+
next unless source_row
|
|
395
|
+
cells = terminal_row(source_row)
|
|
175
396
|
cells.each_with_index do |cell, column|
|
|
176
397
|
next if cell.width.zero?
|
|
177
398
|
x, y = @terminal_bounds.x + column * @terminal_cell_width, @terminal_bounds.y + row * @terminal_line_height
|
|
@@ -180,7 +401,7 @@ module Canopus
|
|
|
180
401
|
foreground, background = background, foreground if cell.attributes[:inverse]
|
|
181
402
|
rect = Zaniah::Bounds.new(x, y, cell.width * @terminal_cell_width, @terminal_line_height)
|
|
182
403
|
fill(rect, background) if background != @theme[:panel]
|
|
183
|
-
position = [
|
|
404
|
+
position = [source_row, column]
|
|
184
405
|
fill(rect, :selection) if selection && (position <=> selection.first) >= 0 && (position <=> selection.last) <= 0
|
|
185
406
|
next if cell.attributes[:hidden]
|
|
186
407
|
foreground = Zaniah::Color.parse(foreground).opacity(0.6) if cell.attributes[:dim]
|
|
@@ -195,31 +416,61 @@ module Canopus
|
|
|
195
416
|
fill(Zaniah::Bounds.new(x, y + @terminal_line_height / 2, rect.width, 1), foreground) if cell.attributes[:strikethrough]
|
|
196
417
|
end
|
|
197
418
|
end
|
|
419
|
+
if prefix == :terminal && terminal.respond_to?(:commands) && !grid.alternate?
|
|
420
|
+
terminal.commands.each do |command|
|
|
421
|
+
source_row = command.prompt_row - map[:base]
|
|
422
|
+
display_row = terminal_display_for_source(map, source_row)
|
|
423
|
+
next unless display_row
|
|
424
|
+
row = display_row - @terminal_first_display
|
|
425
|
+
next unless row.between?(0, rows - 1)
|
|
426
|
+
y = @terminal_bounds.y + row * @terminal_line_height
|
|
427
|
+
fill(Zaniah::Bounds.new(@terminal_bounds.x, y, @terminal_bounds.width, 1), :border)
|
|
428
|
+
foldable = terminal_fold_interval(command, map)
|
|
429
|
+
marker = foldable ? (terminal_folds(terminal)[command.id] ? "▸ " : "▾ ") : ""
|
|
430
|
+
label = marker + (command.exit_status.zero? ? "✓" : "exit #{command.exit_status}")
|
|
431
|
+
width = label.length * 7 + 8
|
|
432
|
+
text(label, @terminal_bounds.right - width + 4, y, color: command.exit_status.zero? ? :muted : :error, size: 10)
|
|
433
|
+
command_regions << [Zaniah::Bounds.new(@terminal_bounds.right - width, y, width, @terminal_line_height), command, terminal] if foldable
|
|
434
|
+
end
|
|
435
|
+
end
|
|
198
436
|
blinking = @workspace.settings["terminal"]["blinking"]
|
|
199
437
|
if grid.cursor_visible && @terminal_scroll.zero? &&
|
|
200
438
|
(blinking == "off" || @cursor_visible || !@workspace.terminal_composition&.text.to_s.empty?)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
fill(
|
|
213
|
-
|
|
439
|
+
cursor_source = grid.scrollback.length + grid.cursor_y
|
|
440
|
+
cursor_display = terminal_display_for_source(map, cursor_source)
|
|
441
|
+
cursor_row = cursor_display && cursor_display - @terminal_first_display
|
|
442
|
+
if cursor_row&.between?(0, rows - 1)
|
|
443
|
+
x = @terminal_bounds.x + grid.cursor_x * @terminal_cell_width
|
|
444
|
+
y = @terminal_bounds.y + cursor_row * @terminal_line_height
|
|
445
|
+
cursor = case @workspace.settings["terminal"]["cursor_shape"]
|
|
446
|
+
when "bar" then Zaniah::Bounds.new(x, y, 2, @terminal_line_height)
|
|
447
|
+
when "underline" then Zaniah::Bounds.new(x, y + @terminal_line_height - 2, @terminal_cell_width, 2)
|
|
448
|
+
else Zaniah::Bounds.new(x, y, @terminal_cell_width, @terminal_line_height)
|
|
449
|
+
end
|
|
450
|
+
fill(cursor, "#e9a66166")
|
|
451
|
+
composition = @workspace.terminal_composition
|
|
452
|
+
if composition && !composition.text.empty?
|
|
453
|
+
layout = text(composition.text, x, y, color: :accent, size: @terminal_font_size)
|
|
454
|
+
fill(Zaniah::Bounds.new(x, y + @terminal_line_height - 2, layout&.width || 20, 1), :accent)
|
|
455
|
+
@cx.window.ime_state = Zaniah::Bounds.new(x, y, 2, @terminal_line_height)
|
|
456
|
+
end
|
|
214
457
|
end
|
|
215
458
|
end
|
|
216
459
|
end
|
|
217
460
|
if terminal.respond_to?(:status) && terminal.status
|
|
218
461
|
code = terminal.status.exited? ? terminal.status.exitstatus : "signal #{terminal.status.termsig}"
|
|
219
|
-
|
|
462
|
+
hint = prefix == :terminal ? "Process exited with #{code} — press Enter to restart" : "Task exited with #{code}"
|
|
463
|
+
text(hint, @terminal_bounds.x + 4,
|
|
220
464
|
[@terminal_bounds.bottom - @terminal_line_height, @terminal_bounds.y].max, color: :muted, size: 11)
|
|
221
465
|
end
|
|
222
|
-
|
|
466
|
+
if prefix == :terminal && terminal.equal?(@workspace.terminal)
|
|
467
|
+
fill(Zaniah::Bounds.new(bounds.x, bounds.y, bounds.width, 2), :accent)
|
|
468
|
+
end
|
|
469
|
+
action = prefix == :terminal ? [prefix, terminal] : [prefix]
|
|
470
|
+
region(@terminal_bounds, role: :terminal, label: prefix == :terminal ? "Terminal" : "Task output", action: action)
|
|
471
|
+
command_regions.each do |bounds, command, command_terminal|
|
|
472
|
+
region(bounds, role: :button, label: "Toggle command output", action: [:terminal_command, command, command_terminal])
|
|
473
|
+
end
|
|
223
474
|
end
|
|
224
475
|
end
|
|
225
476
|
end
|