canopus 0.1.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 (146) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +148 -0
  5. data/docs/adr/001-runtime-gem-components.md +24 -0
  6. data/docs/adr/002-bounded-diff-engine.md +22 -0
  7. data/docs/adr/003-background-language-analysis.md +22 -0
  8. data/docs/adr/004-persistent-display-map.md +23 -0
  9. data/docs/adr/005-windows-runtime.md +27 -0
  10. data/docs/adr/README.md +36 -0
  11. data/docs/distribution.md +27 -0
  12. data/docs/lsp.md +38 -0
  13. data/docs/performance.md +40 -0
  14. data/docs/snippets.md +32 -0
  15. data/docs/vim.md +24 -0
  16. data/docs/workspace_edits.md +32 -0
  17. data/examples/native_smoke.rb +25 -0
  18. data/examples/plugins/word_count.rb +11 -0
  19. data/exe/canopus +19 -0
  20. data/lib/canopus/block_map.rb +19 -0
  21. data/lib/canopus/buffer.rb +293 -0
  22. data/lib/canopus/cli.rb +171 -0
  23. data/lib/canopus/controller.rb +507 -0
  24. data/lib/canopus/data_compat.rb +25 -0
  25. data/lib/canopus/display_map/line_builder.rb +54 -0
  26. data/lib/canopus/display_map/line_set.rb +8 -0
  27. data/lib/canopus/display_map/pending_line_set.rb +9 -0
  28. data/lib/canopus/display_map/summary.rb +23 -0
  29. data/lib/canopus/display_map/worker.rb +89 -0
  30. data/lib/canopus/display_map.rb +350 -0
  31. data/lib/canopus/display_point.rb +5 -0
  32. data/lib/canopus/editor/snippet_expandable.rb +281 -0
  33. data/lib/canopus/editor.rb +445 -0
  34. data/lib/canopus/error.rb +5 -0
  35. data/lib/canopus/fold_map.rb +87 -0
  36. data/lib/canopus/git/blame.rb +50 -0
  37. data/lib/canopus/git/commit.rb +7 -0
  38. data/lib/canopus/git/corrupt_object.rb +7 -0
  39. data/lib/canopus/git/diff.rb +131 -0
  40. data/lib/canopus/git/index.rb +94 -0
  41. data/lib/canopus/git/object_database.rb +45 -0
  42. data/lib/canopus/git/pack.rb +186 -0
  43. data/lib/canopus/git/repository.rb +313 -0
  44. data/lib/canopus/git/status.rb +74 -0
  45. data/lib/canopus/git/tree_entry.rb +7 -0
  46. data/lib/canopus/git.rb +15 -0
  47. data/lib/canopus/icon_theme.rb +43 -0
  48. data/lib/canopus/language/background_analysis/job.rb +12 -0
  49. data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
  50. data/lib/canopus/language/background_analysis.rb +280 -0
  51. data/lib/canopus/language/definition.rb +7 -0
  52. data/lib/canopus/language/document.rb +143 -0
  53. data/lib/canopus/language/symbol.rb +7 -0
  54. data/lib/canopus/language/syntax_worker.rb +91 -0
  55. data/lib/canopus/language.rb +42 -0
  56. data/lib/canopus/lazy_rope.rb +218 -0
  57. data/lib/canopus/lsp/client.rb +299 -0
  58. data/lib/canopus/lsp/error.rb +7 -0
  59. data/lib/canopus/lsp/future/subscription.rb +9 -0
  60. data/lib/canopus/lsp/future.rb +87 -0
  61. data/lib/canopus/lsp/protocol.rb +83 -0
  62. data/lib/canopus/lsp/server_error.rb +13 -0
  63. data/lib/canopus/lsp/timeout.rb +7 -0
  64. data/lib/canopus/lsp/transport.rb +123 -0
  65. data/lib/canopus/lsp.rb +19 -0
  66. data/lib/canopus/markdown.rb +69 -0
  67. data/lib/canopus/match_data_compat.rb +17 -0
  68. data/lib/canopus/multi_buffer.rb +266 -0
  69. data/lib/canopus/pane.rb +61 -0
  70. data/lib/canopus/patch/composite.rb +12 -0
  71. data/lib/canopus/patch/reload.rb +18 -0
  72. data/lib/canopus/patch.rb +27 -0
  73. data/lib/canopus/performance_recorder.rb +207 -0
  74. data/lib/canopus/plugins/api.rb +50 -0
  75. data/lib/canopus/plugins/isolated_runtime.rb +167 -0
  76. data/lib/canopus/plugins/local_runtime.rb +25 -0
  77. data/lib/canopus/plugins/permission_denied.rb +7 -0
  78. data/lib/canopus/plugins/registry.rb +30 -0
  79. data/lib/canopus/plugins.rb +11 -0
  80. data/lib/canopus/project/ignore_matcher.rb +104 -0
  81. data/lib/canopus/project/search.rb +164 -0
  82. data/lib/canopus/project/search_worker/cancelled.rb +3 -0
  83. data/lib/canopus/project/search_worker/runner.rb +9 -0
  84. data/lib/canopus/project/search_worker.rb +108 -0
  85. data/lib/canopus/project/tree.rb +48 -0
  86. data/lib/canopus/project/watcher.rb +59 -0
  87. data/lib/canopus/project.rb +127 -0
  88. data/lib/canopus/regexp_compat.rb +30 -0
  89. data/lib/canopus/save_conflict.rb +5 -0
  90. data/lib/canopus/selection.rb +11 -0
  91. data/lib/canopus/settings.rb +118 -0
  92. data/lib/canopus/snippet/transform.rb +209 -0
  93. data/lib/canopus/snippet.rb +183 -0
  94. data/lib/canopus/tab_map.rb +30 -0
  95. data/lib/canopus/terminal/cell.rb +7 -0
  96. data/lib/canopus/terminal/grid.rb +321 -0
  97. data/lib/canopus/terminal/pty.rb +103 -0
  98. data/lib/canopus/terminal/scrollback.rb +38 -0
  99. data/lib/canopus/terminal/vt.rb +398 -0
  100. data/lib/canopus/terminal.rb +13 -0
  101. data/lib/canopus/theme.rb +43 -0
  102. data/lib/canopus/version.rb +5 -0
  103. data/lib/canopus/vim/commandable.rb +148 -0
  104. data/lib/canopus/vim/motionable.rb +321 -0
  105. data/lib/canopus/vim/operator_capable.rb +377 -0
  106. data/lib/canopus/vim/text_object_selectable.rb +141 -0
  107. data/lib/canopus/vim.rb +426 -0
  108. data/lib/canopus/workspace/edit/executable.rb +139 -0
  109. data/lib/canopus/workspace/edit/node.rb +8 -0
  110. data/lib/canopus/workspace/edit/plan.rb +170 -0
  111. data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
  112. data/lib/canopus/workspace/edit.rb +6 -0
  113. data/lib/canopus/workspace/file_change_aware.rb +40 -0
  114. data/lib/canopus/workspace/file_previewable.rb +31 -0
  115. data/lib/canopus/workspace/git_aware.rb +140 -0
  116. data/lib/canopus/workspace/language_aware.rb +413 -0
  117. data/lib/canopus/workspace/language_server_configurable.rb +181 -0
  118. data/lib/canopus/workspace/project_searchable.rb +208 -0
  119. data/lib/canopus/workspace/project_tree_editable.rb +82 -0
  120. data/lib/canopus/workspace/session_persistable.rb +153 -0
  121. data/lib/canopus/workspace/settings_aware.rb +91 -0
  122. data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
  123. data/lib/canopus/workspace/view.rb +643 -0
  124. data/lib/canopus/workspace.rb +525 -0
  125. data/lib/canopus/wrap_map.rb +108 -0
  126. data/lib/canopus.rb +24 -0
  127. data/sig/canopus.rbs +375 -0
  128. data/sig/controller.rbs +55 -0
  129. data/sig/display_stages.rbs +49 -0
  130. data/sig/git.rbs +140 -0
  131. data/sig/lsp.rbs +99 -0
  132. data/sig/markdown.rbs +11 -0
  133. data/sig/performance_recorder.rbs +23 -0
  134. data/sig/search_services.rbs +8 -0
  135. data/sig/services.rbs +81 -0
  136. data/sig/snippet.rbs +42 -0
  137. data/sig/terminal.rbs +125 -0
  138. data/sig/vim.rbs +27 -0
  139. data/sig/workspace_edits.rbs +11 -0
  140. data/sig/workspace_services.rbs +117 -0
  141. data/tools/certify_snippet_regex.rb +35 -0
  142. data/tools/check_dependencies.rb +80 -0
  143. data/tools/native_check.rb +70 -0
  144. data/tools/package.rb +113 -0
  145. data/tools/package_test.rb +32 -0
  146. metadata +314 -0
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Canopus
6
+ module Workspace::View::TerminalPresentable
7
+ ANSI_COLORS = %w[#1b1d23 #e06c75 #98c379 #e5c07b #61afef #c678dd #56b6c2 #abb2bf #5c6370 #ff7a85 #b5e890 #ffdb8a #82c7ff #e79aff #79dce7 #ffffff].freeze
8
+ attr_reader :terminal_bounds, :terminal_selection
9
+ Link = Data.define(:kind, :target, :line, :column)
10
+
11
+ def terminal_link_at(point)
12
+ return unless @terminal_bounds&.contains?(point) && @workspace.terminal
13
+ column, row = terminal_point(point)
14
+ cells = terminal_row(@terminal_first + row)
15
+ column -= 1 while column.positive? && cells[column].width.zero?
16
+ cell = cells[column]
17
+ return terminal_link(cell.hyperlink, explicit: true) if cell.hyperlink
18
+ # ponytail: bare-link detection stays within one screen row; OSC 8 links
19
+ # retain their metadata across wrapping and do not have this limit.
20
+ text = cells.map(&:text).join
21
+ offset = cells.first(column).sum { |value| value.text.bytesize }
22
+ text.to_enum(:scan, /[^\s<>"'`]+/).each do
23
+ match = Regexp.last_match
24
+ first = text[0...match.begin(0)].bytesize
25
+ next unless offset >= first && offset < first + match[0].bytesize
26
+ value = match[0].sub(/[.,;]+\z/, "")
27
+ value = value.delete_prefix("(").delete_suffix(")") if value.start_with?("(")
28
+ value = value.delete_suffix(")") if value.count(")") > value.count("(")
29
+ return terminal_link(value)
30
+ end
31
+ nil
32
+ end
33
+
34
+ # This is intentionally an explicit user gesture, never an output handler.
35
+ def terminal_open_link(point, modifiers:)
36
+ return false if (modifiers & %w[cmd ctrl]).empty?
37
+ link = terminal_link_at(point)
38
+ return false unless link
39
+ if link.kind == :url
40
+ return false unless @workspace.window&.respond_to?(:open_url)
41
+ @workspace.window.open_url(link.target)
42
+ else
43
+ editor = @workspace.open(link.target)
44
+ if link.line
45
+ row = (link.line - 1).clamp(0, editor.buffer.line_count - 1)
46
+ value = editor.buffer.line(row)
47
+ prefix = value.each_char.take((link.column.to_i - 1).clamp(0, value.length)).join
48
+ editor.select(editor.buffer.rope.line_start(row) + prefix.bytesize)
49
+ editor.reveal_cursor
50
+ end
51
+ @workspace.window&.request_frame
52
+ end
53
+ true
54
+ end
55
+
56
+ def terminal_color(value, fallback)
57
+ return @theme[fallback] if value.nil?
58
+ return format("#%02x%02x%02x", *value) if value.is_a?(Array)
59
+ return ANSI_COLORS[value] if value < 16
60
+ return format("#%02x%02x%02x", *Array.new(3, 8 + (value - 232) * 10)) if value >= 232
61
+ index = value - 16
62
+ ramp = [0, 95, 135, 175, 215, 255]
63
+ format("#%02x%02x%02x", ramp[index / 36], ramp[index / 6 % 6], ramp[index % 6])
64
+ end
65
+ def terminal_point(point)
66
+ grid = @workspace.terminal.grid
67
+ column = ((point.x - @terminal_bounds.x) / @terminal_cell_width).floor.clamp(0, grid.columns - 1)
68
+ row = ((point.y - @terminal_bounds.y) / @line_height).floor.clamp(0, grid.rows - 1)
69
+ [column, row]
70
+ end
71
+ def terminal_scroll(delta)
72
+ @terminal_scroll = ((@terminal_scroll || 0) - delta).clamp(0, @workspace.terminal.grid.scrollback.length)
73
+ @terminal_selection = nil
74
+ end
75
+ def terminal_select(point, extend: false)
76
+ column, row = terminal_point(point)
77
+ index = [@terminal_first + row, column]
78
+ @terminal_selection = extend && @terminal_selection ? [@terminal_selection.first, index] : [index, index]
79
+ end
80
+ def terminal_selected_text
81
+ return "" unless @terminal_selection
82
+ first, last = @terminal_selection.sort
83
+ (first.first..last.first).map do |row|
84
+ cells = terminal_row(row)
85
+ from = row == first.first ? first.last : 0
86
+ to = row == last.first ? last.last : cells.length - 1
87
+ cells[from..to].to_a.map(&:text).join.rstrip
88
+ end.join("\n")
89
+ end
90
+
91
+ private
92
+ def terminal_link(value, explicit: false)
93
+ return if value.empty? || value.bytesize > 8192 || value.match?(/[\x00-\x1f\x7f]/)
94
+ if value.match?(/\Ahttps?:\/\//i)
95
+ uri = URI.parse(value)
96
+ return Link.new(:url, value, nil, nil) if %w[http https].include?(uri.scheme.downcase) && uri.host && !uri.host.empty?
97
+ return
98
+ end
99
+ line = column = nil
100
+ if value.start_with?("file://")
101
+ uri = URI.parse(value)
102
+ return unless [nil, "", "localhost"].include?(uri.host)
103
+ value = URI::RFC2396_PARSER.unescape(uri.path).force_encoding("UTF-8")
104
+ return unless value.start_with?("/") && value.valid_encoding? && !value.include?("\0")
105
+ if (location = uri.fragment&.match(/\AL?(\d+)(?:[:C](\d+))?\z/))
106
+ line, column = location[1].to_i, location[2]&.to_i
107
+ end
108
+ value = value.delete_prefix("/") if RUBY_PLATFORM.match?(/mingw|mswin/) && value.match?(%r{\A/[A-Za-z]:/})
109
+ elsif value.match?(%r{\A[a-z][a-z0-9+.-]*:}i) && !value.match?(%r{\A[A-Za-z]:[\\/]})
110
+ # Reject executable/custom URI schemes; path:line below is still valid.
111
+ return if explicit || !value.match?(/:\d+(?::\d+)?\z/)
112
+ end
113
+ if (location = value.match(/\A(.+?)(?::(\d+)(?::(\d+))?|\((\d+)(?:,(\d+))?\))\z/))
114
+ value, line, column = location[1], (location[2] || location[4]).to_i, (location[3] || location[5])&.to_i
115
+ end
116
+ directory = @workspace.terminal.vt.cwd
117
+ directory = @workspace.root unless directory && File.directory?(directory)
118
+ path = File.expand_path(value, directory)
119
+ Link.new(:file, path, line, column) if File.file?(path)
120
+ rescue URI::InvalidURIError, ArgumentError, SystemCallError
121
+ nil
122
+ end
123
+
124
+ def terminal_font(cell)
125
+ return unless cell.attributes[:italic] && @cx.text_system
126
+ system = @cx.text_system
127
+ return unless system.respond_to?(:font_db) && system.respond_to?(:font)
128
+ key = [system.font_db.object_id, system.font.family, cell.attributes[:bold] ? 700 : 400]
129
+ @terminal_italic_fonts ||= {}
130
+ @terminal_italic_fonts[key] ||= system.font_db.find(family: key[1], weight: key[2], style: :italic)
131
+ end
132
+
133
+ def terminal_row(index)
134
+ grid = @workspace.terminal.grid
135
+ index < grid.scrollback.length ? grid.scrollback[index] : grid.cells[index - grid.scrollback.length]
136
+ end
137
+ def render_terminal(bounds)
138
+ fill(bounds, :panel)
139
+ terminal = @workspace.terminal
140
+ return unless terminal
141
+ text(terminal.vt.title.to_s.empty? ? "Terminal" : terminal.vt.title, bounds.x + 12, bounds.y + 5, color: :muted, size: 12)
142
+ @terminal_cell_width = @cx.text_system&.layout_line("M", size: @font_size)&.width || @font_size * 0.6
143
+ columns = [(bounds.width - 24) / @terminal_cell_width, 1].max.floor
144
+ rows = [(bounds.height - 28) / @line_height, 1].max.floor
145
+ terminal.resize(columns: columns, rows: rows) if terminal.grid.columns != columns || terminal.grid.rows != rows
146
+ grid = terminal.grid
147
+ @terminal_bounds = Zaniah::Bounds.new(bounds.x + 12, bounds.y + 28, columns * @terminal_cell_width, rows * @line_height)
148
+ @terminal_scroll = (@terminal_scroll || 0).clamp(0, grid.scrollback.length)
149
+ @terminal_first = grid.scrollback.length - @terminal_scroll.floor
150
+ selection = @terminal_selection&.sort
151
+ @scene.clip(@terminal_bounds) do
152
+ rows.times do |row|
153
+ cells = terminal_row(@terminal_first + row)
154
+ cells.each_with_index do |cell, column|
155
+ next if cell.width.zero?
156
+ x, y = @terminal_bounds.x + column * @terminal_cell_width, @terminal_bounds.y + row * @line_height
157
+ foreground = terminal_color(cell.foreground, :foreground)
158
+ background = terminal_color(cell.background, :panel)
159
+ foreground, background = background, foreground if cell.attributes[:inverse]
160
+ rect = Zaniah::Bounds.new(x, y, cell.width * @terminal_cell_width, @line_height)
161
+ fill(rect, background) if background != @theme[:panel]
162
+ position = [@terminal_first + row, column]
163
+ fill(rect, :selection) if selection && (position <=> selection.first) >= 0 && (position <=> selection.last) <= 0
164
+ next if cell.attributes[:hidden]
165
+ foreground = Zaniah::Color.parse(foreground).opacity(0.6) if cell.attributes[:dim]
166
+ unless cell.text.strip.empty?
167
+ font = terminal_font(cell)
168
+ text(cell.text, x, y, color: foreground, font: font)
169
+ text(cell.text, x + 0.5, y, color: foreground, font: font) if cell.attributes[:bold]
170
+ end
171
+ if cell.attributes[:underline] || cell.hyperlink
172
+ fill(Zaniah::Bounds.new(x, y + @line_height - 2, rect.width, 1), terminal_color(cell.attributes[:underline_color], :foreground))
173
+ end
174
+ fill(Zaniah::Bounds.new(x, y + @line_height / 2, rect.width, 1), foreground) if cell.attributes[:strikethrough]
175
+ end
176
+ end
177
+ if grid.cursor_visible && @terminal_scroll.zero?
178
+ fill(Zaniah::Bounds.new(@terminal_bounds.x + grid.cursor_x * @terminal_cell_width,
179
+ @terminal_bounds.y + grid.cursor_y * @line_height, @terminal_cell_width, @line_height), "#e9a66166")
180
+ end
181
+ end
182
+ region(@terminal_bounds, role: :terminal, label: "Terminal", action: [:terminal])
183
+ end
184
+ end
185
+ end