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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +191 -19
  4. data/assets/keymaps/emacs.jsonc +5 -0
  5. data/assets/keymaps/jetbrains.jsonc +5 -0
  6. data/assets/keymaps/sublime.jsonc +5 -0
  7. data/assets/keymaps/vscode.jsonc +5 -0
  8. data/docs/debugging.md +143 -0
  9. data/docs/distribution.md +40 -9
  10. data/docs/lsp.md +131 -0
  11. data/docs/performance.md +18 -0
  12. data/docs/providers.md +36 -0
  13. data/docs/release-log.md +12 -0
  14. data/docs/tasks.md +64 -0
  15. data/docs/testing.md +49 -0
  16. data/docs/vim.md +3 -0
  17. data/exe/canopus +4 -1
  18. data/lib/canopus/buffer.rb +291 -50
  19. data/lib/canopus/cli.rb +5 -2
  20. data/lib/canopus/command.rb +13 -4
  21. data/lib/canopus/controller.rb +183 -40
  22. data/lib/canopus/debug/breakpoints.rb +550 -0
  23. data/lib/canopus/debug/configuration.rb +291 -0
  24. data/lib/canopus/debug/console.rb +243 -0
  25. data/lib/canopus/debug/panel.rb +487 -0
  26. data/lib/canopus/debug/session.rb +354 -0
  27. data/lib/canopus/decoration.rb +4 -2
  28. data/lib/canopus/denebola_compat.rb +27 -0
  29. data/lib/canopus/diagnostics.rb +173 -0
  30. data/lib/canopus/display_map/overlay_map.rb +6 -2
  31. data/lib/canopus/editor/snippet_expandable.rb +3 -2
  32. data/lib/canopus/editor.rb +122 -31
  33. data/lib/canopus/git/state.rb +100 -0
  34. data/lib/canopus/language/background_analysis.rb +93 -14
  35. data/lib/canopus/language/document.rb +70 -34
  36. data/lib/canopus/language/symbol.rb +1 -0
  37. data/lib/canopus/language/syntax_worker.rb +61 -9
  38. data/lib/canopus/language.rb +3 -0
  39. data/lib/canopus/minimap.rb +201 -0
  40. data/lib/canopus/patch.rb +3 -1
  41. data/lib/canopus/plugins/api.rb +4 -2
  42. data/lib/canopus/plugins/isolated_runtime.rb +64 -7
  43. data/lib/canopus/plugins/registry.rb +114 -3
  44. data/lib/canopus/plugins.rb +8 -7
  45. data/lib/canopus/project.rb +15 -6
  46. data/lib/canopus/provider.rb +192 -0
  47. data/lib/canopus/regexp_compat.rb +36 -4
  48. data/lib/canopus/selection.rb +12 -0
  49. data/lib/canopus/settings/schema.rb +201 -0
  50. data/lib/canopus/settings.rb +328 -49
  51. data/lib/canopus/task/configuration.rb +356 -0
  52. data/lib/canopus/task/problem_matcher.rb +264 -0
  53. data/lib/canopus/task/runner.rb +314 -0
  54. data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
  55. data/lib/canopus/test_runner/discovery.rb +97 -0
  56. data/lib/canopus/test_runner/execution.rb +194 -0
  57. data/lib/canopus/test_runner/minitest.rb +116 -0
  58. data/lib/canopus/test_runner/rspec.rb +103 -0
  59. data/lib/canopus/test_runner/safe_walker.rb +37 -0
  60. data/lib/canopus/test_runner.rb +54 -0
  61. data/lib/canopus/theme/import.rb +131 -0
  62. data/lib/canopus/theme.rb +28 -4
  63. data/lib/canopus/version.rb +1 -1
  64. data/lib/canopus/vim/motionable.rb +1 -1
  65. data/lib/canopus/vim.rb +10 -16
  66. data/lib/canopus/workspace/auto_savable.rb +83 -0
  67. data/lib/canopus/workspace/debug_aware.rb +600 -0
  68. data/lib/canopus/workspace/edit/plan.rb +2 -2
  69. data/lib/canopus/workspace/editor_configurable.rb +113 -0
  70. data/lib/canopus/workspace/file_change_aware.rb +11 -3
  71. data/lib/canopus/workspace/git_aware.rb +80 -34
  72. data/lib/canopus/workspace/git_blame.rb +175 -0
  73. data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
  74. data/lib/canopus/workspace/git_diff_view.rb +641 -0
  75. data/lib/canopus/workspace/git_history.rb +171 -0
  76. data/lib/canopus/workspace/git_remote.rb +267 -0
  77. data/lib/canopus/workspace/git_staging.rb +583 -0
  78. data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
  79. data/lib/canopus/workspace/keymap_aware.rb +66 -0
  80. data/lib/canopus/workspace/language_aware.rb +3721 -135
  81. data/lib/canopus/workspace/language_server_configurable.rb +287 -71
  82. data/lib/canopus/workspace/persistent_undo.rb +169 -0
  83. data/lib/canopus/workspace/problems_aware.rb +131 -0
  84. data/lib/canopus/workspace/project_searchable.rb +88 -11
  85. data/lib/canopus/workspace/project_tree_editable.rb +3 -1
  86. data/lib/canopus/workspace/recoverable.rb +204 -0
  87. data/lib/canopus/workspace/session_persistable.rb +85 -13
  88. data/lib/canopus/workspace/settings_aware.rb +76 -2
  89. data/lib/canopus/workspace/task_aware.rb +194 -0
  90. data/lib/canopus/workspace/test_aware.rb +394 -0
  91. data/lib/canopus/workspace/trust.rb +73 -0
  92. data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
  93. data/lib/canopus/workspace/view.rb +399 -74
  94. data/lib/canopus/workspace.rb +688 -44
  95. data/lib/canopus.rb +20 -2
  96. data/sig/canopus.rbs +138 -31
  97. data/sig/controller.rbs +10 -3
  98. data/sig/debug.rbs +121 -0
  99. data/sig/decoration.rbs +12 -2
  100. data/sig/diagnostics.rbs +26 -0
  101. data/sig/minimap.rbs +20 -0
  102. data/sig/provider.rbs +29 -0
  103. data/sig/services.rbs +1 -16
  104. data/sig/task.rbs +92 -0
  105. data/sig/test_runner.rbs +109 -0
  106. data/sig/trust.rbs +17 -0
  107. data/sig/workspace_edits.rbs +2 -2
  108. data/sig/workspace_services.rbs +234 -2
  109. data/tools/check_dependencies.rb +21 -2
  110. data/tools/package.rb +172 -14
  111. data/tools/package_signature_test.rb +33 -0
  112. data/tools/package_test.rb +29 -0
  113. data/tools/sign_package.rb +123 -0
  114. data/tools/update.rb +189 -0
  115. data/tools/update_test.rb +71 -0
  116. metadata +206 -31
  117. data/lib/canopus/lsp/client.rb +0 -307
  118. data/lib/canopus/lsp/error.rb +0 -7
  119. data/lib/canopus/lsp/future/subscription.rb +0 -9
  120. data/lib/canopus/lsp/future.rb +0 -87
  121. data/lib/canopus/lsp/protocol.rb +0 -83
  122. data/lib/canopus/lsp/server_error.rb +0 -13
  123. data/lib/canopus/lsp/timeout.rb +0 -7
  124. data/lib/canopus/lsp/transport.rb +0 -123
  125. data/lib/canopus/lsp.rb +0 -19
  126. data/lib/canopus/project/search.rb +0 -164
  127. data/lib/canopus/project/search_worker/cancelled.rb +0 -3
  128. data/lib/canopus/project/search_worker/runner.rb +0 -9
  129. data/lib/canopus/project/search_worker.rb +0 -108
  130. data/lib/canopus/terminal/cell.rb +0 -7
  131. data/lib/canopus/terminal/grid.rb +0 -321
  132. data/lib/canopus/terminal/pty.rb +0 -178
  133. data/lib/canopus/terminal/scrollback.rb +0 -38
  134. data/lib/canopus/terminal/vt.rb +0 -398
  135. data/lib/canopus/terminal.rb +0 -13
  136. data/sig/lsp.rbs +0 -99
  137. data/sig/terminal.rbs +0 -130
@@ -3,16 +3,15 @@
3
3
  module Canopus
4
4
  # A viewport is one element; only visible document rows become draw commands.
5
5
  class Workspace::View < Zaniah::Element
6
- attr_reader :editor_bounds, :row_layouts, :regions, :accessibility
6
+ attr_reader :editor_bounds, :minimap_bounds, :row_layouts, :regions, :accessibility
7
7
  attr_accessor :keymap
8
8
  def initialize(workspace)
9
9
  super()
10
10
  @workspace = workspace
11
- @editor_bounds, @row_layouts, @regions, @accessibility = {}, {}, [], []
11
+ @editor_bounds, @minimap_bounds, @row_layouts, @regions, @accessibility = {}, {}, {}, [], []
12
12
  @project_scroll = 0
13
13
  reset_blink
14
14
  @revealed_cursors, @line_widths, @code_caches = {}, {}, {}
15
- @frame_diagnostics = {}
16
15
  end
17
16
  def reset_blink(now = Process.clock_gettime(Process::CLOCK_MONOTONIC))
18
17
  @blink_started, @cursor_visible = now, true
@@ -33,12 +32,15 @@ module Canopus
33
32
  @font_size = @workspace.settings["font_size"]
34
33
  @line_height = (@font_size * 1.55).ceil
35
34
  @line_height = 20 if @cx.window.is_a?(Zaniah::Platform::TUI::Window)
35
+ @frame_id = @frame_id.to_i + 1
36
+ @workspace.minimap.begin_frame(@frame_id, text_system: @cx.text_system, font_size: @font_size,
37
+ font_family: @workspace.settings["font_family"], scale_factor: @cx.window.scale_factor)
36
38
  @regions.clear
37
39
  @row_layouts.clear
38
40
  @overlay_rows = []
39
41
  @editor_bounds.clear
42
+ @minimap_bounds.clear
40
43
  @accessibility.clear
41
- @frame_diagnostics.clear
42
44
  visible_editors = @workspace.panes.map(&:active)
43
45
  @revealed_cursors.delete_if { |editor, _| !visible_editors.include?(editor) }
44
46
  @line_widths.delete_if { |editor, _| !visible_editors.include?(editor) }
@@ -46,7 +48,7 @@ module Canopus
46
48
  fill(bounds, :background)
47
49
  left_panels = drawable_panels(:left)
48
50
  bottom_panels = drawable_panels(:bottom)
49
- bottom_visible = @workspace.docks[:bottom][:visible] && (@workspace.terminal_visible || !bottom_panels.empty?)
51
+ bottom_visible = @workspace.docks[:bottom][:visible] && (@workspace.task_output_visible || @workspace.terminal_visible || !bottom_panels.empty?)
50
52
  bottom = bottom_visible ? [bounds.height * 0.7, @workspace.docks[:bottom][:size]].min : 0
51
53
  left_visible = @workspace.docks[:left][:visible] && (@workspace.show_project || !left_panels.empty?)
52
54
  sidebar = left_visible && bounds.width >= 620 ? [@workspace.docks[:left][:size], bounds.width * 0.4].min : 0
@@ -63,7 +65,13 @@ module Canopus
63
65
  paint_layout(@workspace.layout, body)
64
66
  if bottom.positive?
65
67
  area = Zaniah::Bounds.new(sidebar, body.height, body.width, bottom)
66
- @workspace.terminal_visible ? paint_terminal(area) : paint_panels(:bottom, area)
68
+ if @workspace.task_output_visible
69
+ paint_task_output(area)
70
+ elsif @workspace.terminal_visible
71
+ paint_terminal(area)
72
+ else
73
+ paint_panels(:bottom, area)
74
+ end
67
75
  end
68
76
  paint_panels(:right, Zaniah::Bounds.new(bounds.width - right, 0, right, bounds.height - 26)) if right.positive?
69
77
  region(Zaniah::Bounds.new(sidebar - 3, 0, 6, body.height), role: :separator, label: "Resize left dock", action: [:dock_resize, :left, bounds]) if sidebar.positive?
@@ -101,6 +109,18 @@ module Canopus
101
109
  def project_scroll(delta)
102
110
  @project_scroll = [@project_scroll + delta, 0].max
103
111
  end
112
+ def minimap_scroll(editor, point)
113
+ entry = @minimap_bounds[editor]
114
+ return false unless entry && @workspace.panes.include?(entry[:pane]) && entry[:pane].active.equal?(editor)
115
+
116
+ bounds = entry[:bounds]
117
+ fraction = ((point.y - bounds.y).to_f / [bounds.height, 1].max).clamp(0, 1)
118
+ source_row = (fraction * editor.buffer.line_count).floor.clamp(0, editor.buffer.line_count - 1)
119
+ display_row = editor.display_map.to_display(editor.buffer.rope.line_start(source_row)).row
120
+ target = (display_row - editor.viewport_rows / 2.0).clamp(0, [editor.display_map.row_count - editor.viewport_rows, 0].max)
121
+ editor.scroll(dy: target - editor.scroll_y)
122
+ true
123
+ end
104
124
 
105
125
  private
106
126
  def paint_notifications(bounds)
@@ -197,7 +217,7 @@ module Canopus
197
217
  end
198
218
  end
199
219
  def drawable_panels(side)
200
- @workspace.panels.active(side).reject { |definition| %w[terminal explorer search].include?(definition.id) }
220
+ @workspace.panels.active(side).reject { |definition| %w[terminal output explorer search].include?(definition.id) }
201
221
  end
202
222
  def paint_layout(node, bounds)
203
223
  return paint_pane(node[:pane], bounds) if node[:pane]
@@ -222,7 +242,8 @@ module Canopus
222
242
  @scene.clip(bounds) do
223
243
  x = bounds.x
224
244
  pane.editors.each do |editor|
225
- name = editor.buffer.path ? File.basename(editor.buffer.path) : "Untitled"
245
+ name = editor.buffer.instance_variable_get(:@display_name) ||
246
+ (editor.buffer.path ? File.basename(editor.buffer.path) : "Untitled")
226
247
  name += " *" if editor.buffer.dirty?
227
248
  name = "[#{name}]" if pane.pinned.include?(editor)
228
249
  width = [[name.length * 7.5 + 30, 100].max, 240].min
@@ -247,19 +268,36 @@ module Canopus
247
268
  return
248
269
  end
249
270
  area = Zaniah::Bounds.new(bounds.x, bounds.y + 34, bounds.width, [bounds.height - 34, 0].max)
250
- @editor_bounds[editor] = area
251
- region(area, role: :textbox, label: editor.buffer.path || "Untitled document", action: [:editor, pane, editor])
252
- paint_editor(editor, area, pane == @workspace.active_pane)
271
+ minimap = @workspace.minimap_settings(editor)
272
+ minimap_width = if minimap["enabled"] && !@cx.window.is_a?(Zaniah::Platform::TUI::Window) &&
273
+ area.height.positive? && area.width >= minimap["width"] + gutter(editor) + 80
274
+ minimap["width"]
275
+ else 0
276
+ end
277
+ editor_area = Zaniah::Bounds.new(area.x, area.y, area.width - minimap_width, area.height)
278
+ minimap_area = Zaniah::Bounds.new(editor_area.right, area.y, minimap_width, area.height)
279
+ breadcrumbs = @workspace.breadcrumb_context(editor)
280
+ breadcrumb_height = breadcrumbs && editor_area.height >= @line_height * 2 ? @line_height : 0
281
+ content = Zaniah::Bounds.new(editor_area.x, editor_area.y + breadcrumb_height, editor_area.width,
282
+ [editor_area.height - breadcrumb_height, 0].max)
283
+ decorations = prepare_editor_map(editor, content)
284
+ sticky = prepare_sticky_scroll(editor, content)
285
+ sticky_height = sticky.length * @line_height
286
+ body = Zaniah::Bounds.new(content.x, content.y + sticky_height, content.width,
287
+ [content.height - sticky_height, 0].max)
288
+ @editor_bounds[editor] = body
289
+ region(body, role: :textbox, label: editor.buffer.path || "Untitled document", action: [:editor, pane, editor])
290
+ overview = paint_editor(editor, body, pane == @workspace.active_pane, decorations)
291
+ paint_sticky_scroll(editor, pane,
292
+ Zaniah::Bounds.new(content.x, content.y, content.width, sticky_height), sticky) unless sticky.empty?
293
+ paint_breadcrumbs(editor, pane,
294
+ Zaniah::Bounds.new(editor_area.x, editor_area.y, editor_area.width, breadcrumb_height), breadcrumbs) if breadcrumb_height.positive?
295
+ paint_minimap(editor, pane, minimap_area, overview, minimap) if minimap_width.positive?
253
296
  fill(Zaniah::Bounds.new(bounds.right - 1, bounds.y, 1, bounds.height), :border)
254
297
  end
255
298
  def gutter(editor) = [editor.buffer.line_count.to_s.length * @font_size * 0.6 + 24, 52].max
256
- def paint_editor(editor, bounds, active)
257
- @selection_spans = {}
258
- editor.viewport_rows = [(bounds.height / @line_height).floor, 1].max
259
- map, first = editor.display_map, editor.scroll_y.floor
260
- last = [first + editor.viewport_rows + 1, map.row_count].min
261
- cursor_offset = @workspace.settings["vim_mode"] && active ? @workspace.vim.cursor_position : editor.primary.head
262
- cursor = map.to_display(cursor_offset)
299
+ def prepare_editor_map(editor, bounds)
300
+ map = editor.display_map
263
301
  if map.wrap_map.width && !editor.buffer.read_only
264
302
  width = [bounds.width - gutter(editor) - 16, 1].max
265
303
  if @cx.window.is_a?(Zaniah::Platform::TUI::Window)
@@ -272,19 +310,76 @@ module Canopus
272
310
  @workspace.message = "Soft wrap disabled: #{error.message}"
273
311
  end
274
312
  end
275
- last = [first + editor.viewport_rows + 1, map.row_count].min
276
- cursor = map.to_display(cursor_offset)
277
313
  end
314
+ prepare_editor_decorations(editor, bounds)
315
+ end
316
+ def prepare_editor_decorations(editor, bounds)
317
+ map = editor.display_map
318
+ rows = [(bounds.height / @line_height).floor, 1].max
319
+ first = editor.scroll_y.clamp(0, [map.row_count - rows, 0].max).floor
320
+ last = [first + rows + 1, map.row_count].min
278
321
  decorations = decorations_for_display_rows(editor, map, first, last)
322
+ overlays = decorations.select { |item| %i[inline block].include?(item.kind) }
323
+ if map.set_overlays(overlays, font: @cx.text_system.respond_to?(:font) ? @cx.text_system.font : nil,
324
+ font_size: @font_size, line_height: @line_height)
325
+ first = editor.scroll_y.clamp(0, [map.row_count - rows, 0].max).floor
326
+ last = [first + rows + 1, map.row_count].min
327
+ decorations = (decorations + decorations_for_display_rows(editor, map, first, last)).uniq.sort_by(&:priority).freeze
328
+ overlays = decorations.select { |item| %i[inline block].include?(item.kind) }
329
+ map.set_overlays(overlays, font: @cx.text_system.respond_to?(:font) ? @cx.text_system.font : nil,
330
+ font_size: @font_size, line_height: @line_height)
331
+ end
332
+ decorations
333
+ end
334
+ def prepare_sticky_scroll(editor, bounds)
335
+ rows = [(bounds.height / @line_height).floor, 1].max
336
+ sticky, scroll = [], editor.scroll_y
337
+ loop do
338
+ candidate = @workspace.sticky_context(editor, scroll.floor)
339
+ candidate = candidate.last([[rows - 1, 0].max, candidate.length].min)
340
+ viewport = [rows - candidate.length, 1].max
341
+ clamped = scroll.clamp(0, [editor.display_map.row_count - viewport, 0].max)
342
+ break sticky = candidate if candidate == sticky && clamped == scroll
343
+ sticky, scroll = candidate, clamped
344
+ end
345
+ editor.viewport_rows = [rows - sticky.length, 1].max
346
+ editor.scroll(dy: scroll - editor.scroll_y)
347
+ sticky
348
+ end
349
+ def paint_editor(editor, bounds, active, decorations = nil)
350
+ @selection_spans = {}
351
+ editor.viewport_rows = [(bounds.height / @line_height).floor, 1].max
352
+ editor.scroll
353
+ map, first = editor.display_map, editor.scroll_y.floor
354
+ last = [first + editor.viewport_rows + 1, map.row_count].min
355
+ vim_cursor = @workspace.settings["vim_mode"] && active
356
+ cursor_offsets = vim_cursor ? [@workspace.vim.cursor_position] : editor.selections.map(&:head)
357
+ cursor_offset = cursor_offsets.last
358
+ cursor = map.to_display(cursor_offset)
359
+ cursors = cursor_offsets.map { |offset| map.to_display(offset) }
360
+ decorations ||= decorations_for_display_rows(editor, map, first, last)
279
361
  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)
362
+ font: @cx.text_system.respond_to?(:font) ? @cx.text_system.font : nil,
363
+ font_size: @font_size, line_height: @line_height)
364
+ editor.scroll
365
+ first = editor.scroll_y.floor
281
366
  last = [first + editor.viewport_rows + 1, map.row_count].min
282
367
  cursor = map.to_display(cursor_offset)
283
- decorations = decorations_for_display_rows(editor, map, first, last)
368
+ cursors = cursor_offsets.map { |offset| map.to_display(offset) }
369
+ decorations = (decorations + decorations_for_display_rows(editor, map, first, last)).uniq.sort_by(&:priority).freeze
284
370
  end
285
371
  gutter_items = decorations.select { |item| item.kind == :gutter }
286
372
  highlight_items = decorations.select { |item| item.kind == :highlight }
287
373
  line_items = decorations.select { |item| item.kind == :line }
374
+ foreground_items, guide_items = highlight_items.partition do |item|
375
+ item.style.is_a?(Hash) && item.style[:foreground]
376
+ end
377
+ guide_items, highlight_items = guide_items.partition do |item|
378
+ item.style.is_a?(Hash) && item.style[:guide]
379
+ end
380
+ foreground_by_row = foreground_items.group_by { |item| map.to_display(item.range.begin).row }
381
+ guides_by_row = guide_items.map { |item| [item, map.to_display(item.range.begin)] }
382
+ .group_by { |_item, point| point.row }
288
383
  cursor_row = map.row(cursor.row)
289
384
  cursor_line = @cx.text_system&.layout_line(cursor_row.text, size: @font_size)
290
385
  brackets = []
@@ -316,12 +411,15 @@ module Canopus
316
411
  style = item.style.is_a?(Hash) ? item.style : {color: item.style, rows: 1}
317
412
  next unless source_row.between?(item.row, item.row + style.fetch(:rows, 1) - 1)
318
413
 
319
- marker = Zaniah::Bounds.new(bounds.x + 2, y - 1, 3, @line_height)
414
+ marker = Zaniah::Bounds.new(bounds.x + style.fetch(:gutter_offset, 2), y - 1,
415
+ style.fetch(:gutter_width, 3), @line_height)
320
416
  paint_instrumented_fill(marker, style.fetch(:color, :accent),
321
417
  "syrma:decoration:gutter:#{source_row}:#{instrumentation_component(item.source)}")
322
418
  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])
419
+ action = item.on_click.respond_to?(:right_click) ? :context_decoration : :decoration
420
+ region(Zaniah::Bounds.new(bounds.x + style.fetch(:hit_offset, 0), y - 1,
421
+ style.fetch(:hit_width, 10), @line_height), role: :button,
422
+ label: item.content.to_s, action: [action, item.on_click, editor, source_row])
325
423
  end
326
424
  end
327
425
  number = editor.relative_line_numbers ? (source_row - editor.buffer.rope.point_at(editor.primary.head).row).abs : source_row + 1
@@ -340,7 +438,7 @@ module Canopus
340
438
  fill(Zaniah::Bounds.new(left + x, y + @line_height - 2, width, 2), :accent)
341
439
  end
342
440
  if row.kind == :text && line && @cx.text_system
343
- spans = code_spans(editor, source_row, row)
441
+ spans = code_spans(editor, source_row, row, foreground_by_row[index] || [])
344
442
  @cx.text_system.paint_line(@scene, line, x: left, y: y + line.ascent, color: @theme[:foreground], spans: spans)
345
443
  @cx.window.text_runs << [left, y, row.text, @theme[:foreground]]
346
444
  elsif row.kind == :overlay_block
@@ -353,16 +451,21 @@ module Canopus
353
451
  end
354
452
  text(row.text, left, y, color: color)
355
453
  end
356
- paint_diagnostics(editor, index, row, line, left, y) if row.kind == :text
454
+ paint_whitespace(editor, map, index, source_row, row, line, left, y, bounds) if row.kind == :text
357
455
  paint_overlays&.call
358
- if cursor.row == index && active
359
- x = left + column_x(row.text, line, cursor.column)
360
- width = @workspace.settings["vim_mode"] && @workspace.vim.mode != :insert ? @font_size * 0.6 : 1.5
361
- fill(Zaniah::Bounds.new(x, y, width, @line_height - 2), width > 2 ? "#e9a66177" : :cursor) if @cursor_visible || editor.composition
362
- @cx.window.ime_state = Zaniah::Bounds.new(x, y, 2, @line_height)
363
- if editor.composition && !editor.composition.text.empty?
364
- composition = text(editor.composition.text, x, y, color: :accent)
365
- fill(Zaniah::Bounds.new(x, y + @line_height - 2, composition&.width || 20, 1), :accent)
456
+ paint_indent_guides(row, line, left, y, guides_by_row[index] || [])
457
+ if active
458
+ cursors.each_with_index do |caret, caret_index|
459
+ next unless caret.row == index
460
+ x = left + column_x(row.text, line, caret.column)
461
+ width = vim_cursor && @workspace.vim.mode != :insert ? @font_size * 0.6 : 1.5
462
+ fill(Zaniah::Bounds.new(x, y, width, @line_height - 2), width > 2 ? "#e9a66177" : :cursor) if @cursor_visible || editor.composition
463
+ next unless caret_index == cursors.length - 1
464
+ @cx.window.ime_state = Zaniah::Bounds.new(x, y, 2, @line_height)
465
+ if editor.composition && !editor.composition.text.empty?
466
+ composition = text(editor.composition.text, x, y, color: :accent)
467
+ fill(Zaniah::Bounds.new(x, y + @line_height - 2, composition&.width || 20, 1), :accent)
468
+ end
366
469
  end
367
470
  end
368
471
  end
@@ -370,10 +473,130 @@ module Canopus
370
473
  previous_width = @line_widths[editor]
371
474
  measured_width = [measured_width, previous_width.last].max if previous_width && previous_width.first == editor.buffer.version
372
475
  @line_widths[editor] = [editor.buffer.version, measured_width]
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 })
476
+ overview = @workspace.decorations.items_for(editor.buffer, 0...editor.buffer.line_count)
477
+ paint_scrollbars(editor, bounds, measured_width, overview)
478
+ overview
479
+ end
480
+ def paint_sticky_scroll(editor, pane, bounds, symbols)
481
+ fill(bounds, :panel)
482
+ left = bounds.x + gutter(editor) - editor.scroll_x
483
+ generation = @workspace.document_symbol_generation(editor)
484
+ @scene.clip(bounds) do
485
+ symbols.each_with_index do |symbol, index|
486
+ y = bounds.y + index * @line_height
487
+ row = editor.buffer.rope.point_at(symbol.selection.begin).row
488
+ text(row + 1, bounds.x + 12, y + 4, color: :muted, size: @font_size - 1)
489
+ text(symbol.name, left, y + 4, color: :foreground)
490
+ area = Zaniah::Bounds.new(bounds.x, y, bounds.width, @line_height)
491
+ region(area, role: :button, label: "Go to #{symbol.name}",
492
+ action: [:sticky, pane, editor, symbol.selection.begin, editor.buffer.version, generation])
493
+ end
494
+ end
495
+ fill(Zaniah::Bounds.new(bounds.x, bounds.bottom - 1, bounds.width, 1), :border)
496
+ end
497
+ def paint_breadcrumbs(editor, pane, bounds, context)
498
+ fill(bounds, :panel)
499
+ items = context[:items].first(3)
500
+ slot = bounds.width / items.length
501
+ @scene.clip(bounds) do
502
+ items.each_with_index do |item, index|
503
+ area = Zaniah::Bounds.new(bounds.x + slot * index, bounds.y, slot, bounds.height)
504
+ inset = index.zero? ? 8 : 20
505
+ text("›", area.x + 5, area.y + 3, color: :muted, size: 12) unless index.zero?
506
+ label = clip_breadcrumb(item[:label], [area.width - inset - 6, 0].max, 12)
507
+ next if label.empty?
508
+
509
+ text(label, area.x + inset, area.y + 3, color: :foreground, size: 12)
510
+ region(area, role: :button, label: "Choose siblings for #{item[:label]}",
511
+ action: [:breadcrumb, pane, editor, item, context])
512
+ end
513
+ end
514
+ fill(Zaniah::Bounds.new(bounds.x, bounds.bottom - 1, bounds.width, 1), :border)
375
515
  end
376
- def paint_scrollbars(editor, bounds, content_width, gutter_items)
516
+ def paint_minimap(editor, pane, bounds, overview, settings)
517
+ fill(bounds, :panel)
518
+ @minimap_bounds[editor] = {pane: pane, bounds: bounds}.freeze
519
+ rows = minimap_rows(editor.buffer.line_count, bounds.height)
520
+ @scene.clip(bounds) do
521
+ rows.each do |row|
522
+ texture = @workspace.minimap.texture(editor.buffer, row, width: bounds.width.to_i)
523
+ next unless texture
524
+
525
+ y = bounds.y + bounds.height * row / [editor.buffer.line_count, 1].max
526
+ @scene.sprite(bounds.x, y, bounds.width, 2, texture: texture, color: @theme[:muted])
527
+ end
528
+ paint_minimap_markers(editor, bounds, overview, settings)
529
+ paint_minimap_viewport(editor, bounds)
530
+ end
531
+ region(bounds, role: :scrollbar, label: "Document minimap", action: [:minimap, pane, editor])
532
+ fill(Zaniah::Bounds.new(bounds.x, bounds.y, 1, bounds.height), :border)
533
+ @cx.window.request_frame if @workspace.minimap.pending?
534
+ end
535
+ def minimap_rows(line_count, height)
536
+ count = [[(height / 2).floor, line_count, Minimap::ENTRY_LIMIT].min, 1].max
537
+ return [0] if count == 1
538
+
539
+ count.times.map { |index| index * (line_count - 1) / (count - 1) }.uniq
540
+ end
541
+ def paint_minimap_markers(editor, bounds, overview, settings)
542
+ pixels = {}
543
+ overview.each_with_index do |item, index|
544
+ break if index >= 10_000
545
+ if item.source == :git && item.row
546
+ style = item.style.is_a?(Hash) ? item.style : {color: item.style, rows: 1}
547
+ minimap_marker(pixels, bounds, editor.buffer.line_count, item.row, style.fetch(:rows, 1), style.fetch(:color, :accent))
548
+ elsif settings["show_diagnostics"] && item.source == :diagnostics && item.range
549
+ row = editor.buffer.rope.point_at(item.range.begin).row
550
+ style = item.style.is_a?(Hash) ? item.style : {color: item.style}
551
+ minimap_marker(pixels, bounds, editor.buffer.line_count, row, 1, style.fetch(:color, :error))
552
+ end
553
+ end
554
+ @workspace.minimap.search_rows(editor.buffer, editor.buffer.version).each do |row|
555
+ minimap_marker(pixels, bounds, editor.buffer.line_count, row, 1, :accent)
556
+ end
557
+ pixels.each { |y, color| fill(Zaniah::Bounds.new(bounds.right - 5, y, 5, 1), color) }
558
+ end
559
+ def minimap_marker(pixels, bounds, line_count, row, count, color)
560
+ first = (bounds.y + bounds.height * row / [line_count, 1].max).floor
561
+ last = (bounds.y + bounds.height * (row + count) / [line_count, 1].max).ceil - 1
562
+ first.clamp(bounds.y.ceil, bounds.bottom.ceil - 1).upto(last.clamp(bounds.y.ceil, bounds.bottom.ceil - 1)) do |y|
563
+ pixels[y] = color
564
+ end
565
+ end
566
+ def paint_minimap_viewport(editor, bounds)
567
+ map = editor.display_map
568
+ first = editor.scroll_y.floor.clamp(0, map.row_count - 1)
569
+ last = [first + editor.viewport_rows - 1, map.row_count - 1].min
570
+ top = bounds.y + bounds.height * map.source_row(first) / [editor.buffer.line_count, 1].max
571
+ bottom = bounds.y + bounds.height * (map.source_row(last) + 1) / [editor.buffer.line_count, 1].max
572
+ height = [bottom - top, 8].max.clamp(0, bounds.bottom - top)
573
+ @scene.quad(bounds.x, top, bounds.width, height, color: "#ffffff12", border_width: 1, border_color: @theme[:accent])
574
+ end
575
+ def clip_breadcrumb(value, maximum, size)
576
+ source = value.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).scrub
577
+ return "" unless maximum.positive?
578
+
579
+ layout = @cx.text_system&.layout_line(source, size: size)
580
+ width = layout&.width || Zaniah::Unicode.width(source) * size * 0.6
581
+ return source if width <= maximum
582
+
583
+ ellipsis = "…"
584
+ ellipsis_width = @cx.text_system&.layout_line(ellipsis, size: size)&.width || size * 0.6
585
+ return "" if ellipsis_width > maximum
586
+
587
+ shown = +""
588
+ bytes = 0
589
+ shown_width = 0
590
+ source.each_grapheme_cluster do |grapheme|
591
+ bytes += grapheme.bytesize
592
+ grapheme_width = layout ? layout.x_for_index(bytes) : shown_width + Zaniah::Unicode.width(grapheme) * size * 0.6
593
+ break if grapheme_width + ellipsis_width > maximum
594
+ shown << grapheme
595
+ shown_width = grapheme_width
596
+ end
597
+ shown << ellipsis
598
+ end
599
+ def paint_scrollbars(editor, bounds, content_width, decorations)
377
600
  track = Zaniah::Bounds.new(bounds.right - 9, bounds.y, 9, [bounds.height - 9, 0].max)
378
601
  total = editor.display_map.row_count
379
602
  maximum = [total - editor.viewport_rows, 0].max
@@ -381,15 +604,15 @@ module Canopus
381
604
  height = [24, track.height * editor.viewport_rows / total].max.clamp(0, track.height)
382
605
  thumb = Zaniah::Bounds.new(track.x + 2, track.y + (track.height - height) * editor.scroll_y / maximum, 5, height)
383
606
  fill(thumb, :muted)
384
- gutter_items.first(500).each do |item|
607
+ decorations.select { |item| item.kind == :gutter }.first(500).each do |item|
385
608
  y = track.y + track.height * item.row / [editor.buffer.line_count, 1].max
386
609
  fill(Zaniah::Bounds.new(track.x, y, 3, 2), :accent)
387
610
  end
388
- frame_diagnostics(editor.buffer).first(500).each do |diagnostic|
389
- row = diagnostic.dig("range", "start", "line")
390
- next unless row.is_a?(Integer) && row >= 0
611
+ decorations.select { |item| item.source == :diagnostics && item.kind == :highlight }.first(500).each do |item|
612
+ row = editor.buffer.rope.point_at(item.range.begin).row
613
+ style = item.style.is_a?(Hash) ? item.style : {}
391
614
  y = track.y + track.height * row / [editor.buffer.line_count, 1].max
392
- fill(Zaniah::Bounds.new(track.x + 5, y, 4, 2), :error)
615
+ fill(Zaniah::Bounds.new(track.x + 5, y, 4, 2), style.fetch(:color, :error))
393
616
  end
394
617
  region(track, role: :scrollbar, label: "Vertical scroll", action: [:scrollbar, editor, :vertical, track, thumb, maximum])
395
618
  end
@@ -405,6 +628,61 @@ module Canopus
405
628
  def column_x(value, line, column)
406
629
  line ? line.x_for_index(value[0, column].to_s.bytesize) : column * @font_size * 0.6
407
630
  end
631
+ def paint_whitespace(editor, map, index, source_row, row, line, left, y, bounds)
632
+ settings = @workspace.settings
633
+ language = settings["languages"].fetch(editor.language_document.definition.name, {})
634
+ mode = language.fetch("render_whitespace", settings["render_whitespace"])
635
+ ideographic = language.fetch("render_ideographic_space", settings["render_ideographic_space"])
636
+ return if mode == "none" && !ideographic
637
+
638
+ rope = editor.buffer.rope
639
+ source_lines = {source_row => [rope.line_start(source_row), rope.line(source_row)]}
640
+ byte = column = cells = 0
641
+ row.text.each_grapheme_cluster do |grapheme|
642
+ if grapheme == " " || grapheme == " "
643
+ offset = map.to_buffer(DisplayPoint.new(index, column))
644
+ point = rope.point_at(offset)
645
+ start, source = source_lines[point.row] ||= [rope.line_start(point.row), rope.line(point.row)]
646
+ local = offset - start
647
+ character = source.byteslice(local..)&.each_char&.first
648
+ marker = whitespace_marker(character, mode, ideographic, source, local, offset, editor.selections)
649
+ if marker && (character != "\t" || whitespace_origin?(map, index, column, offset))
650
+ x = line ? line.x_for_index(byte) : cells * @font_size * 0.6
651
+ right = line ? line.x_for_index(byte + grapheme.bytesize) : x + Zaniah::Unicode.width(grapheme) * @font_size * 0.6
652
+ text(marker, left + x, y, color: :muted) if left + right > bounds.x + gutter(editor) && left + x < bounds.right
653
+ end
654
+ end
655
+ byte += grapheme.bytesize
656
+ column += grapheme.length
657
+ cells += grapheme.ascii_only? ? grapheme.length : Zaniah::Unicode.width(grapheme)
658
+ end
659
+ end
660
+ def whitespace_origin?(map, row, column, offset)
661
+ point = map.to_display(offset)
662
+ point == DisplayPoint.new(row, column) || column.zero? && point.row + 1 == row &&
663
+ point.column == map.row(point.row).text.length
664
+ end
665
+ def whitespace_marker(character, mode, ideographic, source, local, offset, selections)
666
+ return "□" if character == " " && ideographic
667
+ return unless character == " " || character == "\t"
668
+ return unless mode == "all" ||
669
+ mode == "boundary" && (character == "\t" || whitespace_boundary?(source, local)) ||
670
+ mode == "selection" && selections.any? { |selection| !selection.empty? && selection.start < offset + character.bytesize && offset < selection.end }
671
+
672
+ character == "\t" ? "→" : "·"
673
+ end
674
+ def whitespace_boundary?(source, offset)
675
+ following = offset + 1
676
+ offset.zero? || following == source.bytesize || whitespace_before?(source, offset) || whitespace_at?(source, following)
677
+ end
678
+ def whitespace_before?(source, offset)
679
+ byte = source.getbyte(offset - 1)
680
+ byte == 32 || byte == 9 || source.byteslice(offset - 3, 3) == " "
681
+ end
682
+ def whitespace_at?(source, offset)
683
+ byte = source.getbyte(offset)
684
+ byte == 32 || byte == 9 || source.byteslice(offset, 3) == " "
685
+ end
408
686
  def decorations_for_display_rows(editor, map, first, last)
409
687
  rows = (first...last).flat_map do |index|
410
688
  row = map.row(index)
@@ -487,23 +765,33 @@ module Canopus
487
765
  Zaniah::Text.new(ui_text(item.content), size: [@font_size - 2, 1].max, color: color)
488
766
  end
489
767
  element = Zaniah::Div.new.w(width).h(height).items_center.child(content)
768
+ element.style(padding: [0, style.fetch(:padding_right, 0), 0, style.fetch(:padding_left, 0)])
490
769
  element.bg(style[:background]) if style[:background]
491
770
  element
492
771
  end
493
772
  def register_overlay_region(editor, item, bounds)
494
773
  return unless item.on_click
495
774
  offset = item.range&.begin || editor.buffer.rope.line_start(item.row)
775
+ action = item.on_click.respond_to?(:right_click) ? :context_decoration : :decoration
496
776
  region(bounds, role: :button, label: item.content.to_s,
497
- action: [:decoration, item.on_click, editor, offset])
777
+ action: [action, item.on_click, editor, offset])
498
778
  end
499
- def code_spans(editor, source_row, row)
779
+ def code_spans(editor, source_row, row, highlight_items = [])
500
780
  return [] if editor.buffer.rope.respond_to?(:lazy?) && editor.buffer.rope.lazy?
501
781
  raw = editor.language_document.tokens_for(source_row)
502
782
  semantic_state = @workspace.semantic_styles&.[](editor.buffer)
503
783
  semantic_state = nil unless semantic_state && semantic_state.first == editor.buffer.version
784
+ base = editor.buffer.rope.line_start(source_row)
785
+ foreground = highlight_items.each_with_object({}) do |item, colors|
786
+ style = item.style.is_a?(Hash) ? item.style : nil
787
+ next unless item.range && style&.[](:foreground)
788
+ next unless item.range.begin.between?(base + row.offsets.first, base + row.offsets.last)
789
+ colors[item.range.begin - base] = @theme[style.fetch(:color)]
790
+ end
504
791
  cache = @code_caches[editor] ||= {}
505
792
  cached = cache[row.object_id]
506
- return cached[3] if cached && cached[4].equal?(row) && cached[0] == raw && cached[1].equal?(@theme) && cached[2].equal?(semantic_state)
793
+ return cached[4] if cached && cached[5].equal?(row) && cached[0] == raw && cached[1].equal?(@theme) &&
794
+ cached[2].equal?(semantic_state) && cached[3] == foreground
507
795
  offset = 0
508
796
  tokens = raw.map do |token, value|
509
797
  start = offset
@@ -519,6 +807,7 @@ module Canopus
519
807
  color = tokens[index] && original >= tokens[index][0] ? tokens[index][2] : @theme[:foreground]
520
808
  overlay = semantic.find { |first, last, _| original >= first && original < last }
521
809
  color = overlay[2] if overlay
810
+ color = foreground[original] if foreground.key?(original)
522
811
  if spans.last && spans.last[2] == color
523
812
  spans.last[1] += length
524
813
  else
@@ -528,27 +817,9 @@ module Canopus
528
817
  end
529
818
  cache.shift if cache.length >= 512
530
819
  spans.each(&:freeze).freeze
531
- cache[row.object_id] = [raw, @theme, semantic_state, spans, row]
820
+ cache[row.object_id] = [raw, @theme, semantic_state, foreground, spans, row]
532
821
  spans
533
822
  end
534
- def paint_diagnostics(editor, index, row, line, left, y)
535
- frame_diagnostics(editor.buffer).each do |diagnostic|
536
- range = diagnostic["range"]
537
- first = editor.display_map.to_display(LSP::Protocol.offset(editor.buffer.rope, range.fetch("start")))
538
- last = editor.display_map.to_display(LSP::Protocol.offset(editor.buffer.rope, range.fetch("end")))
539
- next unless index.between?(first.row, last.row)
540
- from = index == first.row ? first.column : 0
541
- to = index == last.row ? last.column : row.text.length
542
- x = column_x(row.text, line, from)
543
- width = [column_x(row.text, line, to) - x, 6].max
544
- @scene.underline(left + x, y + @line_height - 2, width, color: @theme[:error], wave: true)
545
- rescue RangeError, KeyError
546
- next # Stale server positions must not break a frame after a local edit.
547
- end
548
- end
549
- def frame_diagnostics(buffer)
550
- @frame_diagnostics[buffer] ||= @workspace.diagnostics_for(buffer)
551
- end
552
823
  def paint_highlights(editor, index, row, line, left, y, items)
553
824
  items.each do |item|
554
825
  range = item.range
@@ -561,7 +832,9 @@ module Canopus
561
832
  from = row.offsets.bsearch_index { |offset| offset >= first } || row.text.length
562
833
  to = row.offsets.bsearch_index { |offset| offset >= last } || row.text.length
563
834
  x = column_x(row.text, line, from)
564
- fill(Zaniah::Bounds.new(left + x, y - 1, column_x(row.text, line, to) - x, @line_height), item.style)
835
+ width = column_x(row.text, line, to) - x
836
+ paint_highlight(item, left + x, y, width)
837
+ register_highlight_region(editor, item, left + x, y, width)
565
838
  next
566
839
  end
567
840
  span = highlight_span(editor, item, index)
@@ -575,8 +848,39 @@ module Canopus
575
848
  before && right > before[0] && right <= before[1] ? 0 : 3,
576
849
  after && right > after[0] && right <= after[1] ? 0 : 3,
577
850
  after && x >= after[0] && x < after[1] ? 0 : 3]
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)
851
+ paint_highlight(item, left + x, y, right - x, radii: radii)
852
+ register_highlight_region(editor, item, left + x, y, right - x)
853
+ end
854
+ end
855
+ def register_highlight_region(editor, item, x, y, width)
856
+ return unless item.on_click && width.positive?
857
+
858
+ region(Zaniah::Bounds.new(x, y - 1, width, @line_height), role: :link, label: item.content.to_s,
859
+ action: [:decoration, item.on_click, editor, item.range.begin])
860
+ end
861
+ def paint_indent_guides(row, line, left, y, items)
862
+ items.each do |item, point|
863
+ style = item.style
864
+ x = column_x(row.text, line, point.column)
865
+ color = style.fetch(:color, :muted)
866
+ color = @theme[color] if color.is_a?(Symbol)
867
+ if @cx.window.is_a?(Zaniah::Platform::TUI::Window)
868
+ column = [point.column - 1, 0].max
869
+ text("│", left + column_x(row.text, line, column), y, color: color)
870
+ else
871
+ @scene.quad(left + x, y - 1, style[:active] ? 2 : 1, @line_height, color: color)
872
+ end
873
+ end
874
+ end
875
+ def paint_highlight(item, x, y, width, radii: 0)
876
+ style = item.style.is_a?(Hash) ? item.style : {}
877
+ color = style.fetch(:color, item.style.is_a?(Symbol) ? item.style : :selection)
878
+ color = @theme[color] if color.is_a?(Symbol)
879
+ if style[:underline]
880
+ @scene.underline(x, y + @line_height - 2, [width, 6].max, color: color,
881
+ thickness: style.fetch(:thickness, 1), wave: style[:underline] == :wave)
882
+ else
883
+ @scene.quad(x, y - 1, width, @line_height, color: color, radius: radii)
580
884
  end
581
885
  end
582
886
  def highlight_span(editor, item, index)
@@ -592,11 +896,16 @@ module Canopus
592
896
  x = column_x(row.text, line, from)
593
897
  right = column_x(row.text, line, [to, row.text.length].min)
594
898
  right += @font_size * 0.6 if to > row.text.length
899
+ minimum = item.style.is_a?(Hash) && item.style[:underline] && (!item.on_click || to > from) ? 6 : 0
900
+ right = [right, x + minimum].max
595
901
  cache[index] = right > x ? [x, right] : nil
596
902
  end
597
903
  def paint_terminal(bounds)
598
904
  render_terminal(bounds)
599
905
  end
906
+ def paint_task_output(bounds)
907
+ render_task_output(bounds)
908
+ end
600
909
  def paint_status(bounds)
601
910
  fill(bounds, :status)
602
911
  editor = @workspace.editor
@@ -607,13 +916,20 @@ module Canopus
607
916
  branch = @workspace.git&.branch
608
917
  left = "#{branch} #{left}" if branch
609
918
  left = ":#{@workspace.vim.command_line}" if @workspace.settings["vim_mode"] && @workspace.vim.command_line
610
- diagnostics = frame_diagnostics(editor.buffer).length
919
+ diagnostic_counts = @workspace.diagnostics.counts
920
+ diagnostics = "#{diagnostic_counts[:error].zero? ? '' : "E#{diagnostic_counts[:error]} "}" \
921
+ "#{diagnostic_counts[:warning].zero? ? '' : "W#{diagnostic_counts[:warning]} "}"
611
922
  language = editor.language_document.definition.name
612
- lsp = @workspace.clients[language]
613
- right = "#{diagnostics.zero? ? '' : "!#{diagnostics} "}#{language}#{lsp ? " LSP:#{lsp.state}" : ''} #{point.row + 1}:#{point.column + 1} #{editor.use_tabs ? 'Tab' : 'Spaces'}:#{editor.tab_size} #{editor.buffer.encoding.name}"
923
+ servers = @workspace.language_server_states(language)
924
+ lsp = servers.empty? ? "" : " LSP:#{servers.map { |server| "#{server[:name]}=#{server[:state]}" }.join(',')}"
925
+ newline = editor.buffer.mixed_line_endings? ? "mixed" : editor.buffer.newline.to_s.upcase
926
+ right = "#{diagnostics}#{language}#{lsp} #{point.row + 1}:#{point.column + 1} #{editor.use_tabs ? 'Tab' : 'Spaces'}:#{editor.tab_size} #{editor.buffer.encoding.name} #{newline}"
614
927
  @scene.clip(bounds) do
615
928
  text(left, 12, bounds.y + 6, color: :foreground, size: 12)
616
- text(right, [bounds.width - right.length * 7.2 - 16, bounds.width * 0.5].max, bounds.y + 6, color: :muted, size: 12)
929
+ x = [bounds.width - right.length * 7.2 - 16, bounds.width * 0.5].max
930
+ text(right, x, bounds.y + 6, color: :muted, size: 12)
931
+ region(Zaniah::Bounds.new(x, bounds.y, bounds.width - x, bounds.height), role: :button,
932
+ label: "Encoding #{editor.buffer.encoding.name}, line endings #{newline}", action: [:buffer_format, editor.buffer])
617
933
  end
618
934
  end
619
935
  def paint_hover(bounds)
@@ -737,7 +1053,11 @@ module Canopus
737
1053
  fill(bounds, "#0007")
738
1054
  @scene.shadow(box.x, box.y, box.width, box.height, color: "#0008", blur: 14)
739
1055
  @scene.quad(box.x, box.y, box.width, box.height, color: @theme[:panel], radius: 8, border_width: 1, border_color: @theme[:border])
740
- labels = {commands: "Command palette", files: "Open file", search: "Find in buffer", save_as: "Save as", confirm_close: "Unsaved changes"}
1056
+ labels = {commands: "Command palette", files: "Open file", search: "Find in buffer", save_as: "Save as",
1057
+ confirm_close: "Unsaved changes", breakpoint_actions: "Breakpoint", breakpoint_edit: "Breakpoint value",
1058
+ git_file_history: "File history", git_commit_history: "Commit history", git_commit_paths: "Changed files",
1059
+ git_revision_compare: "Compare revisions (FROM..TO)", git_remotes: "Git remote",
1060
+ git_credentials: palette[:stage] == :username ? "Git username (blank for token)" : "Git credential"}
741
1061
  title = labels.fetch(palette[:kind], palette[:kind].to_s)
742
1062
  if palette[:search_options]
743
1063
  title += " Ctrl+Alt " + {regexp: "R:regex", case_sensitive: "C:case", whole_word: "W:word", selection_only: "S:selection"}.filter_map do |key, label|
@@ -745,7 +1065,8 @@ module Canopus
745
1065
  end.join(" ")
746
1066
  end
747
1067
  @scene.clip(box) { text(title, box.x + 16, box.y + 12, color: :muted, size: palette[:search_options] ? 10 : 12) }
748
- text(palette[:query].empty? ? "Type here…" : palette[:query], box.x + 16, box.y + 36, color: palette[:query].empty? ? :muted : :foreground)
1068
+ query = palette_query_text(palette)
1069
+ text(query.empty? ? "Type here…" : query, box.x + 16, box.y + 36, color: query.empty? ? :muted : :foreground)
749
1070
  @scene.clip(box) do
750
1071
  matches.each_with_index do |match, index|
751
1072
  row = Zaniah::Bounds.new(box.x + 6, box.y + 74 + index * 28, list_width - 12, 28)
@@ -774,6 +1095,10 @@ module Canopus
774
1095
  end
775
1096
  end
776
1097
  end
1098
+
1099
+ def palette_query_text(palette)
1100
+ palette[:secret] && !palette[:query].empty? ? "•" * [palette[:query].length, 64].min : palette[:query]
1101
+ end
777
1102
  end
778
1103
  end
779
1104