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,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Canopus
6
+ module Terminal
7
+ class VT
8
+ DEC_GRAPHICS = {"`" => "◆", "a" => "▒", "f" => "°", "g" => "±", "j" => "┘", "k" => "┐",
9
+ "l" => "┌", "m" => "└", "n" => "┼", "o" => "⎺", "p" => "⎻", "q" => "─", "r" => "⎼", "s" => "⎽",
10
+ "t" => "├", "u" => "┤", "v" => "┴", "w" => "┬", "x" => "│", "y" => "≤", "z" => "≥", "{" => "π", "|" => "≠", "}" => "£", "~" => "·"}.freeze
11
+ attr_reader :grid, :title, :cwd, :modes, :replies, :bell_count
12
+
13
+ def initialize(grid = Grid.new, &reply)
14
+ @grid = grid
15
+ @reply = reply
16
+ @replies = []
17
+ @input = +"".b
18
+ @state = :ground
19
+ @sequence = +"".b
20
+ @modes = {7 => true, 25 => true}
21
+ @charsets = [:ascii, :ascii]
22
+ @charset = 0
23
+ @bell_count = 0
24
+ end
25
+
26
+ # The parser retains incomplete UTF-8 and control sequences between reads.
27
+ def feed(bytes)
28
+ @input << bytes.b
29
+ index = 0
30
+ while index < @input.bytesize
31
+ byte = @input.getbyte(index)
32
+ if @state == :ground && byte >= 0xa0
33
+ length = if byte.between?(0xc2, 0xdf) then 2
34
+ elsif byte.between?(0xe0, 0xef) then 3
35
+ elsif byte.between?(0xf0, 0xf4) then 4
36
+ else 1 end
37
+ break if index + length > @input.bytesize
38
+ char = @input.byteslice(index, length).force_encoding(Encoding::UTF_8)
39
+ unless char.valid_encoding?
40
+ char = "\ufffd"
41
+ length = 1
42
+ end
43
+ print(char)
44
+ index += length
45
+ next
46
+ end
47
+ consume(byte)
48
+ index += 1
49
+ end
50
+ @input = @input.byteslice(index..) || +"".b
51
+ self
52
+ end
53
+
54
+ def paste(text)
55
+ modes[2004] ? "\e[200~#{text.gsub("\e[201~", "")}\e[201~" : text
56
+ end
57
+
58
+ def mouse(button:, column:, row:, action: :press, shift: false, alt: false, control: false)
59
+ tracking = [1000, 1002, 1003].find { |mode| modes[mode] }
60
+ return "" unless tracking
61
+ return "" if action == :move && (tracking == 1000 || (tracking == 1002 && button.nil?))
62
+ code = case button
63
+ when :left, 0 then 0
64
+ when :middle, 1 then 1
65
+ when :right, 2 then 2
66
+ when :wheel_up then 64
67
+ when :wheel_down then 65
68
+ else 3 end
69
+ code |= 32 if action == :move
70
+ code |= 4 if shift
71
+ code |= 8 if alt
72
+ code |= 16 if control
73
+ column = [column + 1, 1].max
74
+ row = [row + 1, 1].max
75
+ if modes[1006]
76
+ "\e[<#{code};#{column};#{row}#{action == :release ? 'm' : 'M'}"
77
+ else
78
+ code = (code & ~3) | 3 if action == :release
79
+ return "" if column > 223 || row > 223
80
+ "\e[M".b + [code + 32, column + 32, row + 32].pack("C3")
81
+ end
82
+ end
83
+
84
+ def key(name, shift: false, alt: false, control: false)
85
+ name = name.to_s
86
+ modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (control ? 4 : 0)
87
+ arrows = {"up" => "A", "down" => "B", "right" => "C", "left" => "D", "home" => "H", "end" => "F"}
88
+ if arrows.key?(name)
89
+ return modifier > 1 ? "\e[1;#{modifier}#{arrows[name]}" : "\e#{modes[1] ? 'O' : '['}#{arrows[name]}"
90
+ end
91
+ function = {"insert" => 2, "delete" => 3, "page_up" => 5, "page_down" => 6,
92
+ "f5" => 15, "f6" => 17, "f7" => 18, "f8" => 19, "f9" => 20, "f10" => 21, "f11" => 23, "f12" => 24}
93
+ return "\e[#{function[name]}#{modifier > 1 ? ";#{modifier}" : ''}~" if function.key?(name)
94
+ if /\Af[1-4]\z/.match?(name)
95
+ final = ("P".ord + name[1].to_i - 1).chr
96
+ return modifier > 1 ? "\e[1;#{modifier}#{final}" : "\eO#{final}"
97
+ end
98
+ return "\e[Z" if name == "tab" && shift
99
+ text = {"enter" => "\r", "backspace" => "\x7f", "tab" => "\t", "escape" => "\e"}.fetch(name, name)
100
+ text = (text.upcase.ord & 0x1f).chr if control && text.length == 1 && ("@".."_").cover?(text.upcase)
101
+ text = "\0" if control && text == " "
102
+ alt ? "\e#{text}" : text
103
+ end
104
+
105
+ private
106
+
107
+ def consume(byte)
108
+ if [:osc, :dcs, :string].include?(@state)
109
+ return consume_string(byte)
110
+ end
111
+ if byte == 0x1b
112
+ @state = :escape
113
+ @sequence.clear
114
+ return
115
+ elsif [0x18, 0x1a].include?(byte)
116
+ @state = :ground
117
+ return
118
+ elsif byte < 0x20 || byte == 0x7f
119
+ control(byte)
120
+ return
121
+ end
122
+ case @state
123
+ when :ground
124
+ case byte
125
+ when 0x9b then start_sequence(:csi)
126
+ when 0x9d then start_sequence(:osc)
127
+ when 0x90 then start_sequence(:dcs)
128
+ when 0x84 then grid.linefeed
129
+ when 0x85 then grid.carriage_return; grid.linefeed
130
+ when 0x8d then grid.reverse_index
131
+ else print(byte.chr) if byte < 0x80
132
+ end
133
+ when :escape
134
+ case byte.chr
135
+ when "[" then start_sequence(:csi)
136
+ when "]" then start_sequence(:osc)
137
+ when "P" then start_sequence(:dcs)
138
+ when "_", "^", "X" then start_sequence(:string)
139
+ when "(" then @charset_target = 0; @state = :charset
140
+ when ")" then @charset_target = 1; @state = :charset
141
+ when "#", "%" then @state = :escape_intermediate; @sequence << byte
142
+ else
143
+ escape(byte.chr)
144
+ @state = :ground
145
+ end
146
+ when :escape_intermediate
147
+ if @sequence == "#" && byte == "8".ord
148
+ grid.cells.each { |row| row.each { |cell| cell.text = "E"; cell.width = 1 } }
149
+ end
150
+ @state = :ground
151
+ when :charset
152
+ @charsets[@charset_target] = byte == "0".ord ? :graphics : :ascii
153
+ @state = :ground
154
+ when :csi
155
+ if byte.between?(0x40, 0x7e)
156
+ csi(byte.chr, @sequence)
157
+ @state = :ground
158
+ elsif @sequence.bytesize < 1024 && byte.between?(0x20, 0x3f)
159
+ @sequence << byte
160
+ else
161
+ @state = :csi_ignore
162
+ end
163
+ when :csi_ignore
164
+ @state = :ground if byte.between?(0x40, 0x7e)
165
+ end
166
+ end
167
+
168
+ def start_sequence(state)
169
+ @state = state
170
+ @sequence = +"".b
171
+ @string_escape = @string_overflow = false
172
+ @string_utf8_remaining = 0
173
+ end
174
+
175
+ def consume_string(byte)
176
+ if @string_utf8_remaining.positive? && byte.between?(0x80, 0xbf)
177
+ @string_utf8_remaining -= 1
178
+ append_string(byte)
179
+ return
180
+ end
181
+ @string_utf8_remaining = if byte.between?(0xc2, 0xdf) then 1
182
+ elsif byte.between?(0xe0, 0xef) then 2
183
+ elsif byte.between?(0xf0, 0xf4) then 3
184
+ else 0 end
185
+ if byte == 0x9c || (@string_escape && byte == 0x5c) || (byte == 7 && @state == :osc)
186
+ if !@string_overflow
187
+ osc(@sequence) if @state == :osc
188
+ dcs(@sequence) if @state == :dcs
189
+ end
190
+ @state = :ground
191
+ @string_escape = false
192
+ elsif [0x18, 0x1a].include?(byte)
193
+ @state = :ground
194
+ elsif byte == 0x1b
195
+ @string_escape = true
196
+ else
197
+ @sequence << 0x1b if @string_escape && !@string_overflow
198
+ @string_escape = false
199
+ append_string(byte)
200
+ end
201
+ end
202
+
203
+ def append_string(byte)
204
+ @sequence.bytesize < 16_384 ? @sequence << byte : @string_overflow = true
205
+ end
206
+
207
+ def control(byte)
208
+ case byte
209
+ when 7 then @bell_count += 1
210
+ when 8 then grid.backspace
211
+ when 9 then grid.tab
212
+ when 10, 11, 12 then grid.linefeed; grid.carriage_return if modes[20]
213
+ when 13 then grid.carriage_return
214
+ when 14 then @charset = 1
215
+ when 15 then @charset = 0
216
+ end
217
+ end
218
+
219
+ def escape(char)
220
+ case char
221
+ when "7" then grid.save_cursor
222
+ when "8" then grid.restore_cursor
223
+ when "D" then grid.linefeed
224
+ when "E" then grid.carriage_return; grid.linefeed
225
+ when "M" then grid.reverse_index
226
+ when "H" then grid.tab_set
227
+ when "=" then @modes[:keypad] = true
228
+ when ">" then @modes[:keypad] = false
229
+ when "c" then grid.reset; @modes = {7 => true, 25 => true}; @charsets = [:ascii, :ascii]; @charset = 0
230
+ end
231
+ end
232
+
233
+ def print(char)
234
+ char = DEC_GRAPHICS.fetch(char, char) if @charsets[@charset] == :graphics
235
+ grid.put(char)
236
+ @last_printed = char
237
+ end
238
+
239
+ def csi(final, sequence)
240
+ prefix = sequence[/\A[?<=>]/]
241
+ parameters = sequence.sub(/\A[?<=>]/, "").sub(/[\x20-\x2f]+\z/, "")
242
+ values = parameters.split(";", -1).map { |value| value.empty? ? 0 : value.to_i.clamp(0, 1_000_000) }
243
+ values = [0] if values.empty?
244
+ amount = values.first.zero? ? 1 : values.first
245
+ case final
246
+ when "A" then grid.move(y: grid.cursor_y - amount)
247
+ when "B", "e" then grid.move(y: grid.cursor_y + amount)
248
+ when "C", "a" then grid.move(x: grid.cursor_x + amount)
249
+ when "D" then grid.move(x: grid.cursor_x - amount)
250
+ when "E" then grid.move(x: 0, y: grid.cursor_y + amount)
251
+ when "F" then grid.move(x: 0, y: grid.cursor_y - amount)
252
+ when "G", "`" then grid.move(x: amount - 1)
253
+ when "H", "f" then grid.position(amount, values[1].to_i.zero? ? 1 : values[1])
254
+ when "d" then grid.position(amount, grid.cursor_x + 1)
255
+ when "I" then grid.tab([amount, grid.columns].min)
256
+ when "Z" then grid.tab([amount, grid.columns].min, backward: true)
257
+ when "J" then grid.erase_display(values.first)
258
+ when "K" then grid.erase_line(values.first)
259
+ when "L" then grid.insert_lines(amount)
260
+ when "M" then grid.delete_lines(amount)
261
+ when "P" then grid.delete_characters(amount)
262
+ when "@" then grid.insert_characters(amount)
263
+ when "X" then grid.erase_characters(amount)
264
+ when "S" then grid.scroll_up(amount)
265
+ when "T" then grid.scroll_down(amount) unless values.length > 1
266
+ when "r" then grid.margins(amount, values[1].to_i.zero? ? grid.rows : values[1]) unless prefix
267
+ when "s" then grid.save_cursor
268
+ when "u" then grid.restore_cursor
269
+ when "g" then grid.tab_clear(all: values.first == 3)
270
+ when "m" then sgr(parameters) unless prefix
271
+ when "h", "l" then values.each { |mode| set_mode(mode, final == "h", prefix == "?") }
272
+ when "b" then [amount, grid.columns * grid.rows].min.times { grid.put(@last_printed) } if @last_printed
273
+ when "n"
274
+ emit("\e[0n") if values.first == 5
275
+ emit("\e[#{prefix == '?' ? '?' : ''}#{grid.cursor_y + 1};#{grid.cursor_x + 1}R") if values.first == 6
276
+ when "c"
277
+ emit(prefix == ">" ? "\e[>0;1;0c" : "\e[?1;2c")
278
+ when "t"
279
+ emit("\e[8;#{grid.rows};#{grid.columns}t") if values.first == 18
280
+ emit("\e]l#{title}\e\\") if values.first == 21
281
+ when "p"
282
+ if sequence.end_with?("!")
283
+ grid.attributes = {}.freeze
284
+ grid.foreground = grid.background = nil
285
+ grid.insert_mode = grid.origin_mode = false
286
+ grid.autowrap = grid.cursor_visible = true
287
+ grid.margins
288
+ elsif sequence.end_with?("$")
289
+ state = modes.key?(values.first) ? modes[values.first] ? 1 : 2 : 0
290
+ emit("\e[?#{values.first};#{state}$y")
291
+ end
292
+ end
293
+ end
294
+
295
+ def set_mode(mode, enabled, private_mode)
296
+ @modes[mode] = enabled
297
+ if private_mode
298
+ case mode
299
+ when 6 then grid.origin_mode = enabled; grid.position(1, 1)
300
+ when 7 then grid.autowrap = enabled
301
+ when 25 then grid.cursor_visible = enabled
302
+ when 47, 1047 then grid.alternate(enabled, save: false)
303
+ when 1048 then enabled ? grid.save_cursor : grid.restore_cursor
304
+ when 1049 then grid.alternate(enabled)
305
+ when 1000, 1002, 1003
306
+ [1000, 1002, 1003].each { |other| @modes[other] = false unless other == mode } if enabled
307
+ end
308
+ elsif mode == 4
309
+ grid.insert_mode = enabled
310
+ end
311
+ end
312
+
313
+ def sgr(parameters)
314
+ # Colon subparameters are retained for underline styles and ISO RGB.
315
+ groups = parameters.empty? ? ["0"] : parameters.split(";", -1)
316
+ attributes = grid.attributes.dup
317
+ index = 0
318
+ while index < groups.length
319
+ parts = groups[index].split(":", -1)
320
+ value = parts.first.to_i
321
+ case value
322
+ when 0 then attributes.clear; grid.foreground = grid.background = nil
323
+ when 1 then attributes[:bold] = true
324
+ when 2 then attributes[:dim] = true
325
+ when 3 then attributes[:italic] = true
326
+ when 4 then attributes[:underline] = parts[1] ? parts[1].to_i : 1
327
+ when 5, 6 then attributes[:blink] = true
328
+ when 7 then attributes[:inverse] = true
329
+ when 8 then attributes[:hidden] = true
330
+ when 9 then attributes[:strikethrough] = true
331
+ when 21 then attributes[:underline] = 2
332
+ when 22 then attributes.delete(:bold); attributes.delete(:dim)
333
+ when 23 then attributes.delete(:italic)
334
+ when 24 then attributes.delete(:underline)
335
+ when 25 then attributes.delete(:blink)
336
+ when 27 then attributes.delete(:inverse)
337
+ when 28 then attributes.delete(:hidden)
338
+ when 29 then attributes.delete(:strikethrough)
339
+ when 30..37 then grid.foreground = value - 30
340
+ when 40..47 then grid.background = value - 40
341
+ when 90..97 then grid.foreground = value - 90 + 8
342
+ when 100..107 then grid.background = value - 100 + 8
343
+ when 39 then grid.foreground = nil
344
+ when 49 then grid.background = nil
345
+ when 38, 48, 58
346
+ color = nil
347
+ if parts.length > 1
348
+ kind = parts[1].to_i
349
+ color = kind == 5 ? parts[2]&.to_i : kind == 2 && parts.length >= 5 ? parts.last(3).map(&:to_i) : nil
350
+ else
351
+ kind = groups[index + 1]&.to_i
352
+ count = kind == 5 ? 1 : kind == 2 ? 3 : 0
353
+ if count.positive? && index + 1 + count < groups.length
354
+ color = count == 1 ? groups[index + 2].to_i : groups[index + 2, count].map(&:to_i)
355
+ index += count + 1
356
+ end
357
+ end
358
+ color = color.map { |part| part.clamp(0, 255) }.freeze if color.is_a?(Array)
359
+ color = color.clamp(0, 255) if color.is_a?(Integer)
360
+ if color
361
+ value == 38 ? grid.foreground = color : value == 48 ? grid.background = color : attributes[:underline_color] = color
362
+ end
363
+ when 59 then attributes.delete(:underline_color)
364
+ end
365
+ index += 1
366
+ end
367
+ grid.attributes = attributes.freeze
368
+ end
369
+
370
+ def osc(sequence)
371
+ command, payload = sequence.force_encoding(Encoding::UTF_8).scrub.split(";", 2)
372
+ return unless payload
373
+ case command
374
+ when "0", "2" then @title = payload
375
+ when "7"
376
+ uri = URI.parse(payload)
377
+ @cwd = URI::RFC2396_PARSER.unescape(uri.path) if uri.scheme == "file" && uri.path&.start_with?("/")
378
+ when "8"
379
+ _, url = payload.split(";", 2)
380
+ grid.hyperlink = url.nil? || url.empty? ? nil : url.freeze
381
+ end
382
+ rescue URI::InvalidURIError
383
+ nil
384
+ end
385
+
386
+ def dcs(sequence)
387
+ return unless sequence.start_with?("$q")
388
+ request = sequence.delete_prefix("$q")
389
+ answer = {"m" => "0m", "r" => "#{grid.scroll_top + 1};#{grid.scroll_bottom + 1}r"}[request]
390
+ emit("\eP#{answer ? 1 : 0}$r#{answer || request}\e\\")
391
+ end
392
+
393
+ def emit(bytes)
394
+ @reply ? @reply.call(bytes) : @replies << bytes
395
+ end
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "terminal/cell"
4
+ require_relative "terminal/scrollback"
5
+ require_relative "terminal/grid"
6
+ require_relative "terminal/vt"
7
+ require_relative "terminal/pty"
8
+
9
+ module Canopus
10
+ module Terminal
11
+ # Renderers consume Grid#cells; VT only mutates screen state and emits replies.
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ class Theme
5
+ SYNTAX = {"Comment" => "#8f9cae", "Keyword" => "#eaa4c5", "Name.Class" => "#e5cb92", "Name.Function" => "#8ac6df",
6
+ "Literal.String" => "#b6d49c", "Literal.Number" => "#dab38d", "Operator" => "#a7bbdb", "Generic.Heading" => "#e5cb92", "Error" => "#f97583"}.freeze
7
+ LIGHT_SYNTAX = {"Comment" => "#626e7a", "Keyword" => "#853057", "Name.Class" => "#795500", "Name.Function" => "#075b80",
8
+ "Literal.String" => "#356224", "Literal.Number" => "#854b16", "Operator" => "#375780", "Generic.Heading" => "#795500", "Error" => "#aa2438"}.freeze
9
+ DARK = {background: "#161b22", foreground: "#d6dde6", panel: "#10151c", border: "#2c3644",
10
+ active_tab: "#222d3b", muted: "#728399", accent: "#e9a661", selection: "#34547799",
11
+ cursor: "#f4e4c1", current_line: "#ffffff06", error: "#f97583", status: "#202a36"}.freeze
12
+ LIGHT = DARK.merge(background: "#faf8f3", foreground: "#293440", panel: "#eeece6", border: "#cecac1",
13
+ active_tab: "#fff", muted: "#6d7780", selection: "#93bdd477", current_line: "#00000006",
14
+ cursor: "#34465a", status: "#e3e2dc").freeze
15
+ attr_reader :colors, :syntax, :name
16
+ def initialize(name: "Canopus Dark", colors: {}, syntax: {})
17
+ @name, @syntax = name, syntax
18
+ @colors = (name.match?(/light/i) ? LIGHT : DARK).merge(colors.transform_keys(&:to_sym))
19
+ @colors.each_value { |color| Zaniah::Color.parse(color) }
20
+ @token_mapping = (name.match?(/light/i) ? LIGHT_SYNTAX : SYNTAX).merge(@syntax.transform_values { |value| value.is_a?(Hash) ? value["color"] : value }.compact)
21
+ @token_mapping.each_value { |color| Zaniah::Color.parse(color) }
22
+ @token_cache = {}
23
+ end
24
+ def [](key) = @colors.fetch(key)
25
+ def token_color(token)
26
+ name = token.respond_to?(:qualname) ? token.qualname : token.to_s
27
+ @token_cache[name] ||= begin
28
+ key = @token_mapping.keys.select { |prefix| name == prefix || name.start_with?(prefix + ".") }.max_by(&:length)
29
+ key ? @token_mapping[key] : self[:foreground]
30
+ end
31
+ end
32
+ def self.load(path)
33
+ data = Kochab.parse(File.read(path), strict: false).value
34
+ theme = data.fetch("themes", [data]).first
35
+ style = theme.fetch("style", {})
36
+ map = {"editor.background" => :background, "editor.foreground" => :foreground,
37
+ "panel.background" => :panel, "border" => :border, "text.muted" => :muted,
38
+ "status_bar.background" => :status, "editor.active_line.background" => :current_line}
39
+ colors = style.each_with_object({}) { |(key, value), result| result[map[key]] = value if map[key] }
40
+ new(name: theme.fetch("name", "Imported"), colors: colors, syntax: style.fetch("syntax", {}))
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ module Vim::Commandable
5
+ private
6
+
7
+ def command_key(key)
8
+ @command << key
9
+ case key
10
+ when "esc"
11
+ @command_line = nil
12
+ @operator = nil
13
+ @command.clear
14
+ when "backspace" then @command_line = @command_line.grapheme_clusters[0...-1].join
15
+ when "enter"
16
+ command, @command_line = @command_line, nil
17
+ if @command_type == ":"
18
+ ex(command)
19
+ else
20
+ @search = command unless command.empty?
21
+ @search_direction = @command_type == "/" ? 1 : -1
22
+ target = search_target(count: @search_count || 1)
23
+ @search_count = nil
24
+ if target
25
+ @operator ? operate_motion(target) : move_to(target)
26
+ else
27
+ @operator = nil
28
+ end
29
+ end
30
+ @command.clear
31
+ else @command_line << key if key.grapheme_clusters.length == 1
32
+ end
33
+ rescue RegexpError, Error => error
34
+ @message = error.message
35
+ @operator = nil
36
+ end
37
+
38
+ def search_target(reverse: false, count: 1)
39
+ return if !@search || @search.empty?
40
+ matches = @editor.search(Regexp.new(@search))
41
+ return if matches.empty?
42
+ direction = (@search_direction || 1) * (reverse ? -1 : 1)
43
+ offset = cursor_position
44
+ count.times do
45
+ match = direction == 1 ? (matches.find { |range| range.begin > offset } || matches.first) : (matches.reverse.find { |range| range.begin < offset } || matches.last)
46
+ offset = match.begin
47
+ end
48
+ offset
49
+ rescue RegexpError => error
50
+ @message = error.message
51
+ nil
52
+ end
53
+
54
+ def search_next(reverse: false, count: 1)
55
+ target = search_target(reverse: reverse, count: count)
56
+ move_to(target) if target
57
+ end
58
+
59
+ def ex(command)
60
+ if command.match?(/\A\d+\z/)
61
+ move_to(first_nonblank((command.to_i - 1).clamp(0, last_row)))
62
+ elsif command.match?(/\A(?:%|'\<,'\>)?s[^\w\s]/)
63
+ substitute_command(command)
64
+ elsif command.start_with?("set ")
65
+ command.delete_prefix("set ").split.each do |option|
66
+ case option
67
+ when "relativenumber", "rnu" then @editor.relative_line_numbers = true
68
+ when "norelativenumber", "nornu" then @editor.relative_line_numbers = false
69
+ when /\A(?:tabstop|ts|shiftwidth|sw)=(\d+)\z/
70
+ value = Regexp.last_match(1).to_i
71
+ raise Error, "tabstop must be positive" unless value.positive?
72
+ @editor.tab_size = value
73
+ @editor.display_map.tab_size = value
74
+ when "expandtab", "et" then @editor.use_tabs = false
75
+ when "noexpandtab", "noet" then @editor.use_tabs = true
76
+ end
77
+ end
78
+ elsif command == "w" || command.start_with?("w ")
79
+ @on_command ? @on_command.call(command) : @editor.buffer.save(command.length > 2 ? command[2..] : @editor.buffer.path)
80
+ elsif command == "registers" || command == "reg"
81
+ @message = @registers.map { |name, entry| "\"#{name} #{entry.first.inspect}" }.join("\n")
82
+ else
83
+ @on_command&.call(command)
84
+ end
85
+ end
86
+
87
+ def substitute_command(command)
88
+ selected = command.start_with?("'<,'>")
89
+ selected_rows = [row_at(@visual_anchor), row_at(@visual_head)].minmax if selected && @visual_anchor
90
+ all = command.start_with?("%")
91
+ command = command.delete_prefix("%").delete_prefix("'<,'>")
92
+ delimiter = command[1]
93
+ parts, value, escaped = [], +"", false
94
+ command[2..].each_char do |char|
95
+ if escaped
96
+ value << (char == delimiter ? char : "\\#{char}")
97
+ escaped = false
98
+ elsif char == "\\"
99
+ escaped = true
100
+ elsif char == delimiter && parts.length < 2
101
+ parts << value
102
+ value = +""
103
+ else
104
+ value << char
105
+ end
106
+ end
107
+ parts << value
108
+ raise Error, "invalid substitute command" if parts.length < 2
109
+ pattern, replacement, flags = parts
110
+ flags ||= ""
111
+ pattern = @search if pattern.empty?
112
+ raise Error, "no previous search" unless pattern
113
+ regex = Regexp.new(pattern, flags.include?("i") ? Regexp::IGNORECASE : nil)
114
+ @search = pattern
115
+ rows = if all
116
+ 0..last_row
117
+ elsif selected_rows
118
+ selected_rows[0]..selected_rows[1]
119
+ else
120
+ row_at(cursor_position)..row_at(cursor_position)
121
+ end
122
+ last_changed = nil
123
+ changes = rows.filter_map do |row|
124
+ original = @editor.buffer.line(row)
125
+ replacer = lambda do |_matched|
126
+ match = Regexp.last_match
127
+ replacement.gsub(/\\([0-9&\\])|&/) do |token|
128
+ if token == "&"
129
+ match[0]
130
+ elsif token == "\\&" || token == "\\\\"
131
+ token[1]
132
+ else
133
+ match[token[1].to_i] || ""
134
+ end
135
+ end
136
+ end
137
+ result = flags.include?("g") ? original.gsub(regex, &replacer) : original.sub(regex, &replacer)
138
+ unless result == original
139
+ last_changed = row
140
+ [line_start(row)...line_end(row), result]
141
+ end
142
+ end
143
+ @editor.buffer.edit(changes, kind: :vim) unless changes.empty?
144
+ @mode = :normal if selected
145
+ @editor.select(normal_offset(first_nonblank(last_changed))) if last_changed
146
+ end
147
+ end
148
+ end