canopus 0.3.0 → 0.4.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 +8 -0
- data/README.md +8 -2
- data/docs/adr/008-component-domain-boundaries.md +26 -0
- data/docs/adr/009-decoration-overlay-path.md +26 -0
- data/docs/adr/010-overlay-aware-display-map.md +32 -0
- data/lib/canopus/command.rb +109 -0
- data/lib/canopus/controller.rb +30 -39
- data/lib/canopus/decoration.rb +132 -0
- data/lib/canopus/display_map/line_builder.rb +25 -5
- data/lib/canopus/display_map/overlay_map.rb +123 -0
- data/lib/canopus/display_map.rb +25 -4
- data/lib/canopus/lsp/client.rb +19 -11
- data/lib/canopus/panel.rb +104 -0
- data/lib/canopus/settings.rb +29 -5
- data/lib/canopus/tab_map.rb +8 -3
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/workspace/git_aware.rb +17 -1
- data/lib/canopus/workspace/session_persistable.rb +16 -6
- data/lib/canopus/workspace/view.rb +177 -51
- data/lib/canopus/workspace.rb +90 -23
- data/lib/canopus/wrap_map.rb +27 -1
- data/lib/canopus.rb +3 -0
- data/sig/canopus.rbs +7 -5
- data/sig/command.rbs +30 -0
- data/sig/controller.rbs +0 -2
- data/sig/decoration.rbs +26 -0
- data/sig/display_stages.rbs +28 -2
- data/sig/panel.rbs +35 -0
- data/sig/workspace_services.rbs +5 -2
- metadata +15 -5
|
@@ -27,6 +27,7 @@ module Canopus
|
|
|
27
27
|
end
|
|
28
28
|
def paint(bounds, _state, _prepaint, cx)
|
|
29
29
|
@cx, @theme, @scene = cx, @workspace.theme, cx.scene
|
|
30
|
+
@children.clear
|
|
30
31
|
@viewport_bounds = bounds
|
|
31
32
|
@painted_palette = @workspace.palette
|
|
32
33
|
@font_size = @workspace.settings["font_size"]
|
|
@@ -34,6 +35,7 @@ module Canopus
|
|
|
34
35
|
@line_height = 20 if @cx.window.is_a?(Zaniah::Platform::TUI::Window)
|
|
35
36
|
@regions.clear
|
|
36
37
|
@row_layouts.clear
|
|
38
|
+
@overlay_rows = []
|
|
37
39
|
@editor_bounds.clear
|
|
38
40
|
@accessibility.clear
|
|
39
41
|
@frame_diagnostics.clear
|
|
@@ -42,16 +44,21 @@ module Canopus
|
|
|
42
44
|
@line_widths.delete_if { |editor, _| !visible_editors.include?(editor) }
|
|
43
45
|
@code_caches.delete_if { |editor, _| !visible_editors.include?(editor) }
|
|
44
46
|
fill(bounds, :background)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
left_panels = drawable_panels(:left)
|
|
48
|
+
bottom_panels = drawable_panels(:bottom)
|
|
49
|
+
bottom_visible = @workspace.docks[:bottom][:visible] && (@workspace.terminal_visible || !bottom_panels.empty?)
|
|
50
|
+
bottom = bottom_visible ? [bounds.height * 0.7, @workspace.docks[:bottom][:size]].min : 0
|
|
51
|
+
left_visible = @workspace.docks[:left][:visible] && (@workspace.show_project || !left_panels.empty?)
|
|
52
|
+
sidebar = left_visible && bounds.width >= 620 ? [@workspace.docks[:left][:size], bounds.width * 0.4].min : 0
|
|
53
|
+
right = @workspace.docks[:right][:visible] && !drawable_panels(:right).empty? && bounds.width >= 620 ? [@workspace.docks[:right][:size], bounds.width * 0.4].min : 0
|
|
49
54
|
body = Zaniah::Bounds.new(sidebar, 0, bounds.width - sidebar - right, [bounds.height - 26 - bottom, 0].max)
|
|
50
55
|
if sidebar.positive?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
project_height = if @workspace.show_project
|
|
57
|
+
left_panels.empty? ? bounds.height - 26 : (bounds.height - 26) * 0.6
|
|
58
|
+
else 0
|
|
59
|
+
end
|
|
60
|
+
paint_project(Zaniah::Bounds.new(0, 0, sidebar, project_height)) if project_height.positive?
|
|
61
|
+
paint_panels(:left, Zaniah::Bounds.new(0, project_height, sidebar, bounds.height - 26 - project_height)) unless left_panels.empty?
|
|
55
62
|
end
|
|
56
63
|
paint_layout(@workspace.layout, body)
|
|
57
64
|
if bottom.positive?
|
|
@@ -164,27 +171,34 @@ module Canopus
|
|
|
164
171
|
end
|
|
165
172
|
def paint_panels(side, bounds)
|
|
166
173
|
fill(bounds, :panel)
|
|
167
|
-
panels =
|
|
174
|
+
panels = drawable_panels(side)
|
|
168
175
|
return if panels.empty?
|
|
169
|
-
panels.each_with_index do |
|
|
176
|
+
panels.each_with_index do |definition, index|
|
|
170
177
|
height = bounds.height / panels.length
|
|
171
178
|
area = Zaniah::Bounds.new(bounds.x + 8, bounds.y + height * index, [bounds.width - 16, 0].max, height)
|
|
172
|
-
text(
|
|
173
|
-
|
|
179
|
+
text(definition.title, area.x + 4, area.y + 10, color: :muted, size: 12)
|
|
180
|
+
if definition.badge
|
|
181
|
+
badge = Zaniah::Text.new(ui_text(definition.badge), size: 11, color: @theme[:accent])
|
|
182
|
+
.w(36).h(22).test_id("syrma:panel:#{instrumentation_component(definition.id)}:badge")
|
|
183
|
+
paint_element(badge, Zaniah::Bounds.new(area.right - 40, area.y + 5, 36, 22))
|
|
184
|
+
end
|
|
185
|
+
key = [definition.id, @workspace.editor&.buffer&.object_id, @workspace.editor&.buffer&.version, @theme.object_id]
|
|
174
186
|
@panel_cache ||= {}
|
|
175
187
|
@panel_cache.clear if @panel_cache.length > 50
|
|
176
|
-
element = @panel_cache[key] ||=
|
|
177
|
-
element = Zaniah::Text.new(element
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
element = @panel_cache[key] ||= definition.build.call
|
|
189
|
+
element = Zaniah::Text.new(ui_text(element), color: @theme[:foreground]) unless element.is_a?(Zaniah::Element)
|
|
190
|
+
panel = Zaniah::Div.new.w(area.width).h([area.height - 34, 0].max)
|
|
191
|
+
.test_id("syrma:panel:#{instrumentation_component(definition.id)}").child(element)
|
|
180
192
|
@scene.clip(area) do
|
|
181
|
-
|
|
182
|
-
element.paint(root.bounds, nil, nil, @cx)
|
|
193
|
+
paint_element(panel, Zaniah::Bounds.new(area.x, area.y + 34, area.width, [area.height - 34, 0].max))
|
|
183
194
|
end
|
|
184
195
|
rescue StandardError => error
|
|
185
196
|
text(error.message, area.x, area.y + 34, color: :error, size: 12)
|
|
186
197
|
end
|
|
187
198
|
end
|
|
199
|
+
def drawable_panels(side)
|
|
200
|
+
@workspace.panels.active(side).reject { |definition| %w[terminal explorer search].include?(definition.id) }
|
|
201
|
+
end
|
|
188
202
|
def paint_layout(node, bounds)
|
|
189
203
|
return paint_pane(node[:pane], bounds) if node[:pane]
|
|
190
204
|
horizontal = node[:direction] == :horizontal
|
|
@@ -244,7 +258,6 @@ module Canopus
|
|
|
244
258
|
editor.viewport_rows = [(bounds.height / @line_height).floor, 1].max
|
|
245
259
|
map, first = editor.display_map, editor.scroll_y.floor
|
|
246
260
|
last = [first + editor.viewport_rows + 1, map.row_count].min
|
|
247
|
-
marks = @workspace.git_gutter_marks(editor.buffer)
|
|
248
261
|
cursor_offset = @workspace.settings["vim_mode"] && active ? @workspace.vim.cursor_position : editor.primary.head
|
|
249
262
|
cursor = map.to_display(cursor_offset)
|
|
250
263
|
if map.wrap_map.width && !editor.buffer.read_only
|
|
@@ -262,6 +275,16 @@ module Canopus
|
|
|
262
275
|
last = [first + editor.viewport_rows + 1, map.row_count].min
|
|
263
276
|
cursor = map.to_display(cursor_offset)
|
|
264
277
|
end
|
|
278
|
+
decorations = decorations_for_display_rows(editor, map, first, last)
|
|
279
|
+
if map.set_overlays(decorations.select { |item| %i[inline block].include?(item.kind) },
|
|
280
|
+
font: @cx.text_system&.font, font_size: @font_size, line_height: @line_height)
|
|
281
|
+
last = [first + editor.viewport_rows + 1, map.row_count].min
|
|
282
|
+
cursor = map.to_display(cursor_offset)
|
|
283
|
+
decorations = decorations_for_display_rows(editor, map, first, last)
|
|
284
|
+
end
|
|
285
|
+
gutter_items = decorations.select { |item| item.kind == :gutter }
|
|
286
|
+
highlight_items = decorations.select { |item| item.kind == :highlight }
|
|
287
|
+
line_items = decorations.select { |item| item.kind == :line }
|
|
265
288
|
cursor_row = map.row(cursor.row)
|
|
266
289
|
cursor_line = @cx.text_system&.layout_line(cursor_row.text, size: @font_size)
|
|
267
290
|
brackets = []
|
|
@@ -286,35 +309,43 @@ module Canopus
|
|
|
286
309
|
(first...last).each do |index|
|
|
287
310
|
row = map.row(index)
|
|
288
311
|
y = bounds.y + (index - editor.scroll_y) * @line_height + 4
|
|
289
|
-
fill(Zaniah::Bounds.new(bounds.x, y - 2, bounds.width, @line_height), :current_line) if cursor.row == index
|
|
290
312
|
source_row = map.source_row(index)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
fill(Zaniah::Bounds.new(bounds.x, y - 2, bounds.width, @line_height), :current_line) if cursor.row == index
|
|
314
|
+
paint_line_decorations(editor, source_row, y, bounds, line_items)
|
|
315
|
+
gutter_items.each do |item|
|
|
316
|
+
style = item.style.is_a?(Hash) ? item.style : {color: item.style, rows: 1}
|
|
317
|
+
next unless source_row.between?(item.row, item.row + style.fetch(:rows, 1) - 1)
|
|
318
|
+
|
|
319
|
+
marker = Zaniah::Bounds.new(bounds.x + 2, y - 1, 3, @line_height)
|
|
320
|
+
paint_instrumented_fill(marker, style.fetch(:color, :accent),
|
|
321
|
+
"syrma:decoration:gutter:#{source_row}:#{instrumentation_component(item.source)}")
|
|
322
|
+
if row.kind == :text && item.on_click
|
|
323
|
+
region(Zaniah::Bounds.new(bounds.x, y - 1, 10, @line_height), role: :button,
|
|
324
|
+
label: item.content.to_s, action: [:decoration, item.on_click, editor, source_row])
|
|
325
|
+
end
|
|
299
326
|
end
|
|
300
327
|
number = editor.relative_line_numbers ? (source_row - editor.buffer.rope.point_at(editor.primary.head).row).abs : source_row + 1
|
|
301
328
|
number = source_row + 1 if number.zero?
|
|
302
329
|
text(number, bounds.x + 12, y, color: cursor.row == index ? :foreground : :muted, size: @font_size - 1) if row.kind == :text
|
|
303
|
-
line =
|
|
330
|
+
line, paint_overlays = prepare_inline_overlays(editor, row, left, y,
|
|
331
|
+
[bounds.width - gutter(editor), 1].max)
|
|
332
|
+
line ||= @cx.text_system&.layout_line(row.text, size: @font_size)
|
|
304
333
|
measured_width = [measured_width, line&.width || row.text.length * @font_size * 0.6].max
|
|
305
334
|
@row_layouts[[editor, index]] = line
|
|
306
|
-
|
|
335
|
+
paint_highlights(editor, index, row, line, left, y, highlight_items)
|
|
307
336
|
brackets.each do |point|
|
|
308
337
|
next unless point.row == index
|
|
309
338
|
x = column_x(row.text, line, point.column)
|
|
310
339
|
width = [column_x(row.text, line, point.column + 1) - x, 4].max
|
|
311
340
|
fill(Zaniah::Bounds.new(left + x, y + @line_height - 2, width, 2), :accent)
|
|
312
341
|
end
|
|
313
|
-
if row.kind == :text && line
|
|
342
|
+
if row.kind == :text && line && @cx.text_system
|
|
314
343
|
spans = code_spans(editor, source_row, row)
|
|
315
344
|
@cx.text_system.paint_line(@scene, line, x: left, y: y + line.ascent, color: @theme[:foreground], spans: spans)
|
|
316
345
|
@cx.window.text_runs << [left, y, row.text, @theme[:foreground]]
|
|
317
|
-
|
|
346
|
+
elsif row.kind == :overlay_block
|
|
347
|
+
paint_block_overlay(editor, row.metadata, left, y, [bounds.width - gutter(editor), 1].max) if row.metadata
|
|
348
|
+
elsif row.kind != :overlay_block_continuation
|
|
318
349
|
color = if row.kind == :git_diff
|
|
319
350
|
row.text.start_with?("+") ? "#80b987" : row.text.start_with?("-") ? :error : :muted
|
|
320
351
|
else
|
|
@@ -323,6 +354,7 @@ module Canopus
|
|
|
323
354
|
text(row.text, left, y, color: color)
|
|
324
355
|
end
|
|
325
356
|
paint_diagnostics(editor, index, row, line, left, y) if row.kind == :text
|
|
357
|
+
paint_overlays&.call
|
|
326
358
|
if cursor.row == index && active
|
|
327
359
|
x = left + column_x(row.text, line, cursor.column)
|
|
328
360
|
width = @workspace.settings["vim_mode"] && @workspace.vim.mode != :insert ? @font_size * 0.6 : 1.5
|
|
@@ -338,9 +370,10 @@ module Canopus
|
|
|
338
370
|
previous_width = @line_widths[editor]
|
|
339
371
|
measured_width = [measured_width, previous_width.last].max if previous_width && previous_width.first == editor.buffer.version
|
|
340
372
|
@line_widths[editor] = [editor.buffer.version, measured_width]
|
|
341
|
-
|
|
373
|
+
overview = @workspace.decorations.items_for(editor.buffer, 0...editor.buffer.line_count, context: editor)
|
|
374
|
+
paint_scrollbars(editor, bounds, measured_width, overview.select { |item| item.kind == :gutter })
|
|
342
375
|
end
|
|
343
|
-
def paint_scrollbars(editor, bounds, content_width,
|
|
376
|
+
def paint_scrollbars(editor, bounds, content_width, gutter_items)
|
|
344
377
|
track = Zaniah::Bounds.new(bounds.right - 9, bounds.y, 9, [bounds.height - 9, 0].max)
|
|
345
378
|
total = editor.display_map.row_count
|
|
346
379
|
maximum = [total - editor.viewport_rows, 0].max
|
|
@@ -348,8 +381,8 @@ module Canopus
|
|
|
348
381
|
height = [24, track.height * editor.viewport_rows / total].max.clamp(0, track.height)
|
|
349
382
|
thumb = Zaniah::Bounds.new(track.x + 2, track.y + (track.height - height) * editor.scroll_y / maximum, 5, height)
|
|
350
383
|
fill(thumb, :muted)
|
|
351
|
-
|
|
352
|
-
y = track.y + track.height *
|
|
384
|
+
gutter_items.first(500).each do |item|
|
|
385
|
+
y = track.y + track.height * item.row / [editor.buffer.line_count, 1].max
|
|
353
386
|
fill(Zaniah::Bounds.new(track.x, y, 3, 2), :accent)
|
|
354
387
|
end
|
|
355
388
|
frame_diagnostics(editor.buffer).first(500).each do |diagnostic|
|
|
@@ -372,6 +405,97 @@ module Canopus
|
|
|
372
405
|
def column_x(value, line, column)
|
|
373
406
|
line ? line.x_for_index(value[0, column].to_s.bytesize) : column * @font_size * 0.6
|
|
374
407
|
end
|
|
408
|
+
def decorations_for_display_rows(editor, map, first, last)
|
|
409
|
+
rows = (first...last).flat_map do |index|
|
|
410
|
+
row = map.row(index)
|
|
411
|
+
ending = map.to_buffer(DisplayPoint.new(index, row.text.length))
|
|
412
|
+
[map.source_row(index), editor.buffer.rope.point_at(ending).row]
|
|
413
|
+
end
|
|
414
|
+
rows.sort.uniq.slice_when { |left, right| right != left + 1 }.flat_map do |group|
|
|
415
|
+
@workspace.decorations.items_for(editor.buffer, group.first...(group.last + 1), context: editor)
|
|
416
|
+
end.uniq.sort_by(&:priority).freeze
|
|
417
|
+
end
|
|
418
|
+
def prepare_inline_overlays(editor, row, left, y, width)
|
|
419
|
+
return [nil, nil] unless row.kind == :text && row.metadata.is_a?(Array) && !row.metadata.empty?
|
|
420
|
+
|
|
421
|
+
overlay = Zaniah::Text.new(row.text, size: @font_size, color: "#0000", wrap: :none,
|
|
422
|
+
line_height: @line_height).w(width)
|
|
423
|
+
row.metadata.each do |placement|
|
|
424
|
+
overlay.inline_overlay(offset: placement.offset,
|
|
425
|
+
element: decoration_element(editor, placement.item, placement.width, placement.height)
|
|
426
|
+
.test_id("syrma:decoration:inline:#{editor.buffer.rope.point_at(placement.item.range.begin).row}"),
|
|
427
|
+
align: placement.align)
|
|
428
|
+
end
|
|
429
|
+
node = overlay.request_layout(@cx)
|
|
430
|
+
Zaniah::Layout::Engine.new.compute(node, x: left, y: y, width: width, height: @line_height)
|
|
431
|
+
overlay.prepaint(node.bounds, nil, @cx)
|
|
432
|
+
row.metadata.zip(overlay.children).each do |placement, element|
|
|
433
|
+
register_overlay_region(editor, placement.item, element.layout_node.bounds)
|
|
434
|
+
end
|
|
435
|
+
@overlay_rows << overlay
|
|
436
|
+
child(overlay)
|
|
437
|
+
paragraph = overlay.instance_variable_get(:@paragraph)
|
|
438
|
+
painter = -> { overlay.children.each { |child| child.paint(child.layout_node.bounds, nil, nil, @cx) } }
|
|
439
|
+
[paragraph.lines.first.layout, painter]
|
|
440
|
+
end
|
|
441
|
+
def paint_block_overlay(editor, block, left, y, width)
|
|
442
|
+
overlay = Zaniah::Text.new(" ", size: @font_size, color: "#0000", wrap: :none,
|
|
443
|
+
line_height: @line_height).w(width)
|
|
444
|
+
overlay.block_overlay(line: 0, position: :above, height: block.height,
|
|
445
|
+
element: decoration_element(editor, block.item, width, block.height))
|
|
446
|
+
node = overlay.request_layout(@cx)
|
|
447
|
+
Zaniah::Layout::Engine.new.compute(node, x: left, y: y, width: width, height: block.height + @line_height)
|
|
448
|
+
overlay.prepaint(node.bounds, nil, @cx)
|
|
449
|
+
register_overlay_region(editor, block.item, overlay.children.first.layout_node.bounds)
|
|
450
|
+
overlay.children.each { |child| child.paint(child.layout_node.bounds, nil, nil, @cx) }
|
|
451
|
+
@overlay_rows << overlay
|
|
452
|
+
child(overlay)
|
|
453
|
+
end
|
|
454
|
+
def paint_line_decorations(editor, source_row, y, bounds, items)
|
|
455
|
+
items.each do |item|
|
|
456
|
+
row = item.row || (item.range && editor.buffer.rope.point_at(item.range.begin).row)
|
|
457
|
+
next unless row == source_row
|
|
458
|
+
|
|
459
|
+
style = item.style.is_a?(Hash) ? item.style : {color: item.style}
|
|
460
|
+
area = Zaniah::Bounds.new(bounds.x + gutter(editor), y - 2,
|
|
461
|
+
[bounds.width - gutter(editor), 1].max, @line_height)
|
|
462
|
+
paint_instrumented_fill(area, style.fetch(:color, :current_line),
|
|
463
|
+
"syrma:decoration:line:#{source_row}:#{instrumentation_component(item.source)}")
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
def paint_instrumented_fill(bounds, color, test_id)
|
|
467
|
+
color = @theme[color] if color.is_a?(Symbol)
|
|
468
|
+
paint_element(Zaniah::Div.new.w(bounds.width).h(bounds.height).bg(color).test_id(test_id), bounds)
|
|
469
|
+
end
|
|
470
|
+
def paint_element(element, bounds)
|
|
471
|
+
child(element)
|
|
472
|
+
root = element.request_layout(@cx)
|
|
473
|
+
Zaniah::Layout::Engine.new.compute(root, x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height)
|
|
474
|
+
element.prepaint(root.bounds, nil, @cx)
|
|
475
|
+
element.paint(root.bounds, nil, nil, @cx)
|
|
476
|
+
element
|
|
477
|
+
end
|
|
478
|
+
def instrumentation_component(value) = value.to_s
|
|
479
|
+
def ui_text(value) = value.to_s.encode(Encoding::UTF_8)
|
|
480
|
+
def decoration_element(editor, item, width, height)
|
|
481
|
+
style = item.style.is_a?(Hash) ? item.style : {}
|
|
482
|
+
color = style.fetch(:color, item.style.is_a?(Symbol) ? item.style : :muted)
|
|
483
|
+
color = @theme[color] if color.is_a?(Symbol)
|
|
484
|
+
content = if item.content.respond_to?(:request_layout) && item.content.respond_to?(:paint)
|
|
485
|
+
item.content
|
|
486
|
+
else
|
|
487
|
+
Zaniah::Text.new(ui_text(item.content), size: [@font_size - 2, 1].max, color: color)
|
|
488
|
+
end
|
|
489
|
+
element = Zaniah::Div.new.w(width).h(height).items_center.child(content)
|
|
490
|
+
element.bg(style[:background]) if style[:background]
|
|
491
|
+
element
|
|
492
|
+
end
|
|
493
|
+
def register_overlay_region(editor, item, bounds)
|
|
494
|
+
return unless item.on_click
|
|
495
|
+
offset = item.range&.begin || editor.buffer.rope.line_start(item.row)
|
|
496
|
+
region(bounds, role: :button, label: item.content.to_s,
|
|
497
|
+
action: [:decoration, item.on_click, editor, offset])
|
|
498
|
+
end
|
|
375
499
|
def code_spans(editor, source_row, row)
|
|
376
500
|
return [] if editor.buffer.rope.respond_to?(:lazy?) && editor.buffer.rope.lazy?
|
|
377
501
|
raw = editor.language_document.tokens_for(source_row)
|
|
@@ -425,24 +549,25 @@ module Canopus
|
|
|
425
549
|
def frame_diagnostics(buffer)
|
|
426
550
|
@frame_diagnostics[buffer] ||= @workspace.diagnostics_for(buffer)
|
|
427
551
|
end
|
|
428
|
-
def
|
|
429
|
-
|
|
430
|
-
|
|
552
|
+
def paint_highlights(editor, index, row, line, left, y, items)
|
|
553
|
+
items.each do |item|
|
|
554
|
+
range = item.range
|
|
555
|
+
next unless range
|
|
431
556
|
if editor.buffer.rope.respond_to?(:lazy?)
|
|
432
557
|
base = editor.buffer.rope.line_start(index)
|
|
433
|
-
first =
|
|
434
|
-
last =
|
|
558
|
+
first = range.begin - base
|
|
559
|
+
last = range.end - base
|
|
435
560
|
next if last < row.offsets.first || first > row.offsets.last
|
|
436
561
|
from = row.offsets.bsearch_index { |offset| offset >= first } || row.text.length
|
|
437
562
|
to = row.offsets.bsearch_index { |offset| offset >= last } || row.text.length
|
|
438
563
|
x = column_x(row.text, line, from)
|
|
439
|
-
fill(Zaniah::Bounds.new(left + x, y - 1, column_x(row.text, line, to) - x, @line_height),
|
|
564
|
+
fill(Zaniah::Bounds.new(left + x, y - 1, column_x(row.text, line, to) - x, @line_height), item.style)
|
|
440
565
|
next
|
|
441
566
|
end
|
|
442
|
-
span =
|
|
567
|
+
span = highlight_span(editor, item, index)
|
|
443
568
|
next unless span
|
|
444
|
-
before =
|
|
445
|
-
after =
|
|
569
|
+
before = highlight_span(editor, item, index - 1)
|
|
570
|
+
after = highlight_span(editor, item, index + 1)
|
|
446
571
|
x, right = span
|
|
447
572
|
# Only exposed convex corners are rounded; no alpha-overlapping bridge
|
|
448
573
|
# and no convex hull that would highlight unselected source text.
|
|
@@ -450,11 +575,12 @@ module Canopus
|
|
|
450
575
|
before && right > before[0] && right <= before[1] ? 0 : 3,
|
|
451
576
|
after && right > after[0] && right <= after[1] ? 0 : 3,
|
|
452
577
|
after && x >= after[0] && x < after[1] ? 0 : 3]
|
|
453
|
-
|
|
578
|
+
color = item.style.is_a?(Symbol) ? @theme[item.style] : item.style
|
|
579
|
+
@scene.quad(left + x, y - 1, right - x, @line_height, color: color, radius: radii)
|
|
454
580
|
end
|
|
455
581
|
end
|
|
456
|
-
def
|
|
457
|
-
cache = @selection_spans[
|
|
582
|
+
def highlight_span(editor, item, index)
|
|
583
|
+
cache = @selection_spans[item] ||= {first: editor.display_map.to_display(item.range.begin), last: editor.display_map.to_display(item.range.end)}
|
|
458
584
|
return cache[index] if cache.key?(index)
|
|
459
585
|
first, last = cache.values_at(:first, :last)
|
|
460
586
|
return unless index.between?(first.row, last.row)
|
|
@@ -587,8 +713,7 @@ module Canopus
|
|
|
587
713
|
end
|
|
588
714
|
def shortcut_labels
|
|
589
715
|
return {} unless @keymap
|
|
590
|
-
context =
|
|
591
|
-
"vim_mode" => @workspace.settings["vim_mode"] && @workspace.editor ? @workspace.vim.mode.to_s : false}
|
|
716
|
+
context = @workspace.palette&.dig(:command_context) || @workspace.command_context
|
|
592
717
|
seen, labels = {}, {}
|
|
593
718
|
@keymap.bindings.reverse_each do |binding|
|
|
594
719
|
next unless binding.predicate.call(context)
|
|
@@ -627,7 +752,8 @@ module Canopus
|
|
|
627
752
|
fill(row, :active_tab) if index == palette[:index]
|
|
628
753
|
@scene.clip(row) { text(match, row.x + 10, row.y + 5, size: 13) }
|
|
629
754
|
if palette[:kind] == :commands
|
|
630
|
-
|
|
755
|
+
command_index = palette[:indices] ? palette[:indices][index] : index
|
|
756
|
+
shortcut = shortcuts[palette[:command_definitions][command_index].id]
|
|
631
757
|
if shortcut
|
|
632
758
|
label = shortcut.tr("-", " ")
|
|
633
759
|
x = row.right - label.length * 6.5 - 10
|
data/lib/canopus/workspace.rb
CHANGED
|
@@ -8,13 +8,16 @@ require_relative "vim"
|
|
|
8
8
|
require_relative "lsp"
|
|
9
9
|
require_relative "pane"
|
|
10
10
|
require_relative "project/tree"
|
|
11
|
+
require_relative "command"
|
|
12
|
+
require_relative "panel"
|
|
13
|
+
require_relative "decoration"
|
|
11
14
|
|
|
12
15
|
module Canopus
|
|
13
16
|
class Workspace
|
|
14
17
|
ClosedTab = Data.define(:path, :selections, :scroll_x, :scroll_y, :pane_id, :index)
|
|
15
|
-
attr_reader :panes, :active_pane, :buffers, :actions, :settings, :theme, :project, :clients, :root, :docks, :panels
|
|
18
|
+
attr_reader :panes, :active_pane, :buffers, :actions, :commands, :settings, :theme, :project, :clients, :root, :docks, :panels, :decorations
|
|
16
19
|
attr_reader :terminals, :active_terminal_index
|
|
17
|
-
attr_accessor :window, :
|
|
20
|
+
attr_accessor :window, :terminal_composition, :selected_project_path, :performance
|
|
18
21
|
attr_reader :message, :palette
|
|
19
22
|
|
|
20
23
|
def initialize(root: Dir.pwd, settings: nil)
|
|
@@ -24,18 +27,47 @@ module Canopus
|
|
|
24
27
|
@active_pane = @panes.first
|
|
25
28
|
@layout = {pane: @active_pane}
|
|
26
29
|
@theme = Theme.new(name: @settings["theme"])
|
|
27
|
-
@actions
|
|
28
|
-
@
|
|
30
|
+
@actions = @commands = Command::Registry.new
|
|
31
|
+
@clients, @vim_states = {}, {}
|
|
32
|
+
@message = ""
|
|
29
33
|
@project = Project.new(@root) if defined?(Project)
|
|
30
|
-
|
|
31
|
-
@docks =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
dock = @settings["dock"]
|
|
35
|
+
@docks = %i[left right bottom].to_h do |side|
|
|
36
|
+
value = dock.fetch(side.to_s)
|
|
37
|
+
[side, {visible: value["visible"], size: value["size"]}]
|
|
38
|
+
end
|
|
39
|
+
@panels = Panel::Registry.new(@docks)
|
|
40
|
+
@panels.restore(dock.fetch("panels"))
|
|
41
|
+
@panels.register(Panel::Definition.new("terminal", "Terminal", nil, :bottom, -> { terminal }, nil))
|
|
42
|
+
@panels.register(Panel::Definition.new("search", "Search", nil, :left, -> { palette_open(:project_search) }, nil))
|
|
43
|
+
@panels.register(Panel::Definition.new("explorer", "Explorer", nil, :left, -> { project_tree }, nil))
|
|
44
|
+
@decorations = Decoration::Registry.new
|
|
45
|
+
@decorations.register(:selection_match) do |buffer, rows, current|
|
|
46
|
+
selections = current.is_a?(Editor) && current.buffer.equal?(buffer) ? current.selections : buffer.selections
|
|
47
|
+
selections.filter_map do |selection|
|
|
48
|
+
next if selection.empty?
|
|
49
|
+
first = buffer.rope.point_at(selection.start).row
|
|
50
|
+
last = buffer.rope.point_at(selection.end).row
|
|
51
|
+
next if last < rows.begin || first >= rows.end
|
|
52
|
+
|
|
53
|
+
Decoration::Item.new(:highlight, selection.range, nil, nil, :selection, 100, :selection_match, nil)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
@decorations.register(:git) { |buffer, rows| git_decorations(buffer, rows) }
|
|
57
|
+
@languages, @terminals = {}, []
|
|
34
58
|
@active_terminal_index = 0
|
|
35
59
|
@closed_tabs, @terminal_names = [], {}
|
|
36
60
|
register_actions
|
|
37
61
|
end
|
|
38
62
|
def editor = @active_pane.active
|
|
63
|
+
def show_project = @panels.visible?("explorer")
|
|
64
|
+
def show_project=(visible)
|
|
65
|
+
visible ? @panels.show("explorer") : @panels.hide("explorer")
|
|
66
|
+
end
|
|
67
|
+
def terminal_visible = @panels.visible?("terminal") && @docks[:bottom][:visible]
|
|
68
|
+
def terminal_visible=(visible)
|
|
69
|
+
visible ? @panels.show("terminal") : @panels.hide("terminal")
|
|
70
|
+
end
|
|
39
71
|
def terminal = @terminals[@active_terminal_index]
|
|
40
72
|
def terminal=(value)
|
|
41
73
|
@terminals.each { |item| item.close if !item.equal?(value) && item.respond_to?(:close) }
|
|
@@ -306,7 +338,7 @@ module Canopus
|
|
|
306
338
|
queue_limit_bytes: options["queue_limit_bytes"])
|
|
307
339
|
@terminals << created
|
|
308
340
|
@active_terminal_index = @terminals.length - 1
|
|
309
|
-
|
|
341
|
+
self.terminal_visible = true
|
|
310
342
|
resize_terminal(*@terminal_dimensions, final: true) if @terminal_dimensions
|
|
311
343
|
@window&.request_frame
|
|
312
344
|
created
|
|
@@ -352,7 +384,7 @@ module Canopus
|
|
|
352
384
|
else
|
|
353
385
|
@terminals.index(active) || 0
|
|
354
386
|
end
|
|
355
|
-
|
|
387
|
+
self.terminal_visible = false if @terminals.empty? && @settings["terminal"]["hide_when_empty"]
|
|
356
388
|
@window&.request_frame
|
|
357
389
|
current
|
|
358
390
|
end
|
|
@@ -432,11 +464,13 @@ module Canopus
|
|
|
432
464
|
dock[:visible] = !dock[:visible]
|
|
433
465
|
end
|
|
434
466
|
def register_panel(name, side: :left, &render)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
467
|
+
definition = @panels.register(Panel::Definition.new(name, name.to_s, nil, side, render, nil))
|
|
468
|
+
register_action("panel.#{name}") { @panels.show(definition.id) }
|
|
469
|
+
definition
|
|
470
|
+
end
|
|
471
|
+
def register_action(name, description: name, category: name.to_s.split(".", 2).first, condition: "", keybinding: Command::DEFAULT_KEYBINDINGS[name.to_s], &block)
|
|
472
|
+
@commands.register(Command::Definition.new(name.to_s, description, category, condition, block, keybinding))
|
|
438
473
|
end
|
|
439
|
-
def register_action(name, description: name, &block) = @actions.register(name, description: description, &block)
|
|
440
474
|
def definition_for(path)
|
|
441
475
|
@languages.values.find { |definition| definition.extensions.include?(File.extname(path.to_s)) || definition.extensions.include?(File.basename(path.to_s)) } || Language.for_path(path)
|
|
442
476
|
end
|
|
@@ -450,20 +484,32 @@ module Canopus
|
|
|
450
484
|
end
|
|
451
485
|
end
|
|
452
486
|
end
|
|
453
|
-
def
|
|
454
|
-
|
|
487
|
+
def command_context(terminal: false)
|
|
488
|
+
{"Terminal" => terminal, "Editor" => !!editor,
|
|
489
|
+
"vim_mode" => @settings["vim_mode"] && editor ? vim.mode.to_s : false}
|
|
490
|
+
end
|
|
491
|
+
def call(name, *args, context: nil)
|
|
492
|
+
context ||= @active_command_context || command_context
|
|
493
|
+
previous, @active_command_context = @active_command_context, context
|
|
494
|
+
@commands.call(name, *args, context: context)
|
|
455
495
|
@window&.request_frame
|
|
456
496
|
rescue StandardError => error
|
|
457
497
|
@message = error.message
|
|
458
498
|
@window&.request_frame
|
|
499
|
+
ensure
|
|
500
|
+
@active_command_context = previous
|
|
459
501
|
end
|
|
460
502
|
|
|
461
503
|
def theme=(theme)
|
|
462
504
|
@theme = theme.is_a?(Theme) ? theme : Theme.new(name: theme)
|
|
463
505
|
@window&.request_frame
|
|
464
506
|
end
|
|
465
|
-
def palette_open(kind)
|
|
507
|
+
def palette_open(kind, command_ids: nil, context: nil)
|
|
466
508
|
self.palette = {kind: kind, query: +"", index: 0, matches: []}
|
|
509
|
+
if kind == :commands
|
|
510
|
+
@palette[:command_context] = context || @active_command_context || command_context
|
|
511
|
+
@palette[:command_ids] = command_ids if command_ids
|
|
512
|
+
end
|
|
467
513
|
if [:search, :replace_query, :project_search].include?(kind)
|
|
468
514
|
@palette[:search_options] = {regexp: false, case_sensitive: true, whole_word: false, selection_only: false}
|
|
469
515
|
@palette[:selection] = editor.primary.range unless editor.primary.empty?
|
|
@@ -478,6 +524,20 @@ module Canopus
|
|
|
478
524
|
end
|
|
479
525
|
def update_palette
|
|
480
526
|
return unless @palette
|
|
527
|
+
if @palette[:kind] == :commands
|
|
528
|
+
context = @palette.fetch(:command_context)
|
|
529
|
+
definitions = @commands.each(context: context).to_a
|
|
530
|
+
ids = @palette[:command_ids]
|
|
531
|
+
definitions = definitions.select { |definition| ids.include?(definition.id) } if ids
|
|
532
|
+
@palette[:command_definitions] = definitions
|
|
533
|
+
session = @palette[:search] ||= Spica::Index.new(definitions.map(&:title)).session
|
|
534
|
+
session.query = @palette[:query]
|
|
535
|
+
matches = session.matches(12)
|
|
536
|
+
@palette[:indices] = matches.map(&:index)
|
|
537
|
+
@palette[:matches] = matches.map(&:candidate)
|
|
538
|
+
@palette[:index] = @palette[:index].clamp(0, [@palette[:matches].length - 1, 0].max)
|
|
539
|
+
return
|
|
540
|
+
end
|
|
481
541
|
if [:completion, :locations, :symbols, :code_actions, :outline, :branches, :settings_keys, :snippet_choices].include?(@palette[:kind])
|
|
482
542
|
labels = @palette[:all_matches] ||= @palette[:matches].dup
|
|
483
543
|
session = @palette[:search] ||= Spica::Index.new(labels).session
|
|
@@ -489,7 +549,6 @@ module Canopus
|
|
|
489
549
|
return
|
|
490
550
|
end
|
|
491
551
|
@palette[:search] ||= case @palette[:kind]
|
|
492
|
-
when :commands then Spica::Index.new(@actions.entries.keys).session
|
|
493
552
|
when :files
|
|
494
553
|
@finder_index ||= Spica::Index.new(((@recent_files || []).select { |path| File.file?(path) }.map { |path| path.delete_prefix(@root + File::SEPARATOR) } + files).uniq, tie_break: :index)
|
|
495
554
|
@finder_index.session
|
|
@@ -525,12 +584,17 @@ module Canopus
|
|
|
525
584
|
return
|
|
526
585
|
end
|
|
527
586
|
selected = @palette[:matches][@palette[:index]]
|
|
587
|
+
selected_command = if @palette[:kind] == :commands && selected
|
|
588
|
+
index = @palette[:indices] ? @palette[:indices][@palette[:index]] : @palette[:index]
|
|
589
|
+
@palette[:command_definitions][index]
|
|
590
|
+
end
|
|
591
|
+
command_context = @palette[:command_context]
|
|
528
592
|
kind, query, pattern = @palette.values_at(:kind, :query, :pattern)
|
|
529
593
|
self.palette = nil
|
|
530
594
|
if kind == :files && selected
|
|
531
595
|
open(selected)
|
|
532
|
-
elsif kind == :commands &&
|
|
533
|
-
call(
|
|
596
|
+
elsif kind == :commands && selected_command
|
|
597
|
+
call(selected_command.id, context: command_context)
|
|
534
598
|
elsif kind == :search
|
|
535
599
|
pattern, options = search_query(query, search_state)
|
|
536
600
|
matches = editor.search(pattern, **options)
|
|
@@ -668,12 +732,15 @@ module Canopus
|
|
|
668
732
|
register_action("edit.indent") { editor.indent }
|
|
669
733
|
register_action("edit.outdent") { editor.indent(outdent: true) }
|
|
670
734
|
register_action("search.buffer") { palette_open(:search) }
|
|
671
|
-
register_action("search.project") {
|
|
735
|
+
register_action("search.project") { @panels.fetch("search").build.call }
|
|
672
736
|
register_action("search.replace") { palette_open(:replace_query) }
|
|
673
737
|
register_action("settings.open") { open_settings }
|
|
674
738
|
register_action("settings.complete") { settings_completions }
|
|
675
739
|
register_action("language.diagnostics") { show_diagnostics }
|
|
676
|
-
register_action("view.project") { @
|
|
740
|
+
register_action("view.project") { @panels.toggle("explorer") }
|
|
741
|
+
register_action("panel.explorer") { @panels.toggle("explorer") }
|
|
742
|
+
register_action("panel.search") { @panels.fetch("search").build.call }
|
|
743
|
+
register_action("panel.terminal") { @terminals.empty? ? new_terminal : @panels.toggle("terminal") }
|
|
677
744
|
[:left, :right, :bottom].each { |side| register_action("view.dock_#{side}") { toggle_dock(side) } }
|
|
678
745
|
register_action("view.wrap") { editor.display_map.wrap_width = editor.display_map.wrap_map.width ? nil : 100 }
|
|
679
746
|
register_action("view.theme") { self.theme = @theme.name.include?("Dark") ? "Canopus Light" : "Canopus Dark" }
|
|
@@ -682,7 +749,7 @@ module Canopus
|
|
|
682
749
|
clear_vim_states unless @settings["vim_mode"]
|
|
683
750
|
end
|
|
684
751
|
register_action("view.terminal") do
|
|
685
|
-
@terminals.empty? ? new_terminal : @
|
|
752
|
+
@terminals.empty? ? new_terminal : @panels.toggle("terminal")
|
|
686
753
|
end
|
|
687
754
|
register_action("terminal.toggle") { call("view.terminal") }
|
|
688
755
|
register_action("terminal.new") { new_terminal }
|
data/lib/canopus/wrap_map.rb
CHANGED
|
@@ -23,7 +23,9 @@ module Canopus
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
|
-
def transform(text, offsets, checkpoint: nil)
|
|
26
|
+
def transform(text, offsets, checkpoint: nil, overlays: [], tab_map: nil)
|
|
27
|
+
text, offsets, overlays = tab_map.transform(text, offsets, checkpoint: checkpoint, overlays: overlays) if tab_map
|
|
28
|
+
return overlay_rows(text, offsets, overlays, checkpoint) unless overlays.empty?
|
|
27
29
|
return pixel_rows(text, offsets, checkpoint) if @width && @font
|
|
28
30
|
return [[text.freeze, offsets.freeze]] if !@width || (text.ascii_only? && text.length <= @width)
|
|
29
31
|
rows, offset, chunk, positions, cells = [], 0, +"", [offsets.first], 0
|
|
@@ -53,6 +55,30 @@ module Canopus
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
private
|
|
58
|
+
def overlay_rows(text, offsets, overlays, checkpoint)
|
|
59
|
+
checkpoint&.call
|
|
60
|
+
typesetter = @font ? (@typesetters[Thread.current] ||= @layout_template.fork(capacity: 32)) : nil
|
|
61
|
+
scale = @font ? 1.0 : 0.6
|
|
62
|
+
width = @width ? @width * scale : Float::INFINITY
|
|
63
|
+
inline = overlays.map do |overlay|
|
|
64
|
+
Zaniah::TextSystem::Paragraph::InlineOverlay.new(overlay.object_id, overlay.offset,
|
|
65
|
+
(@font ? overlay.width : overlay.cell_width * scale), overlay.height, overlay.align)
|
|
66
|
+
end
|
|
67
|
+
paragraph = Zaniah::TextSystem::Paragraph.new(text, width: width,
|
|
68
|
+
size: @font ? @font_size : 1, line_height: @font ? nil : 1,
|
|
69
|
+
wrap: @width ? :anywhere : :none, typesetter: typesetter, inline_overlays: inline)
|
|
70
|
+
by_key = overlays.to_h { |overlay| [overlay.object_id, overlay] }
|
|
71
|
+
paragraph.lines.each_with_index.map do |line, index|
|
|
72
|
+
first = text.byteslice(0...line.start).to_s.length
|
|
73
|
+
last = first + line.layout.text.length
|
|
74
|
+
placements = paragraph.inline_placements.filter_map do |placement|
|
|
75
|
+
next unless placement.line == index
|
|
76
|
+
by_key.fetch(placement.key).with(offset: placement.offset - line.start)
|
|
77
|
+
end.freeze
|
|
78
|
+
[line.layout.text.freeze, offsets[first..last].freeze, placements]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
def pixel_rows(text, offsets, checkpoint)
|
|
57
83
|
# The template is never used for shaping. Each owner copies its complete
|
|
58
84
|
# typography without reading the rendering thread's mutable caches.
|
data/lib/canopus.rb
CHANGED
|
@@ -11,6 +11,9 @@ require "denebola"
|
|
|
11
11
|
require "zaniah"
|
|
12
12
|
require "porrima"
|
|
13
13
|
require "thuban"
|
|
14
|
+
require_relative "canopus/command"
|
|
15
|
+
require_relative "canopus/panel"
|
|
16
|
+
require_relative "canopus/decoration"
|
|
14
17
|
require_relative "canopus/patch"
|
|
15
18
|
require_relative "canopus/buffer"
|
|
16
19
|
require_relative "canopus/multi_buffer"
|