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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/docs/adr/001-runtime-gem-components.md +24 -0
- data/docs/adr/002-bounded-diff-engine.md +22 -0
- data/docs/adr/003-background-language-analysis.md +22 -0
- data/docs/adr/004-persistent-display-map.md +23 -0
- data/docs/adr/005-windows-runtime.md +27 -0
- data/docs/adr/README.md +36 -0
- data/docs/distribution.md +27 -0
- data/docs/lsp.md +38 -0
- data/docs/performance.md +40 -0
- data/docs/snippets.md +32 -0
- data/docs/vim.md +24 -0
- data/docs/workspace_edits.md +32 -0
- data/examples/native_smoke.rb +25 -0
- data/examples/plugins/word_count.rb +11 -0
- data/exe/canopus +19 -0
- data/lib/canopus/block_map.rb +19 -0
- data/lib/canopus/buffer.rb +293 -0
- data/lib/canopus/cli.rb +171 -0
- data/lib/canopus/controller.rb +507 -0
- data/lib/canopus/data_compat.rb +25 -0
- data/lib/canopus/display_map/line_builder.rb +54 -0
- data/lib/canopus/display_map/line_set.rb +8 -0
- data/lib/canopus/display_map/pending_line_set.rb +9 -0
- data/lib/canopus/display_map/summary.rb +23 -0
- data/lib/canopus/display_map/worker.rb +89 -0
- data/lib/canopus/display_map.rb +350 -0
- data/lib/canopus/display_point.rb +5 -0
- data/lib/canopus/editor/snippet_expandable.rb +281 -0
- data/lib/canopus/editor.rb +445 -0
- data/lib/canopus/error.rb +5 -0
- data/lib/canopus/fold_map.rb +87 -0
- data/lib/canopus/git/blame.rb +50 -0
- data/lib/canopus/git/commit.rb +7 -0
- data/lib/canopus/git/corrupt_object.rb +7 -0
- data/lib/canopus/git/diff.rb +131 -0
- data/lib/canopus/git/index.rb +94 -0
- data/lib/canopus/git/object_database.rb +45 -0
- data/lib/canopus/git/pack.rb +186 -0
- data/lib/canopus/git/repository.rb +313 -0
- data/lib/canopus/git/status.rb +74 -0
- data/lib/canopus/git/tree_entry.rb +7 -0
- data/lib/canopus/git.rb +15 -0
- data/lib/canopus/icon_theme.rb +43 -0
- data/lib/canopus/language/background_analysis/job.rb +12 -0
- data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
- data/lib/canopus/language/background_analysis.rb +280 -0
- data/lib/canopus/language/definition.rb +7 -0
- data/lib/canopus/language/document.rb +143 -0
- data/lib/canopus/language/symbol.rb +7 -0
- data/lib/canopus/language/syntax_worker.rb +91 -0
- data/lib/canopus/language.rb +42 -0
- data/lib/canopus/lazy_rope.rb +218 -0
- data/lib/canopus/lsp/client.rb +299 -0
- data/lib/canopus/lsp/error.rb +7 -0
- data/lib/canopus/lsp/future/subscription.rb +9 -0
- data/lib/canopus/lsp/future.rb +87 -0
- data/lib/canopus/lsp/protocol.rb +83 -0
- data/lib/canopus/lsp/server_error.rb +13 -0
- data/lib/canopus/lsp/timeout.rb +7 -0
- data/lib/canopus/lsp/transport.rb +123 -0
- data/lib/canopus/lsp.rb +19 -0
- data/lib/canopus/markdown.rb +69 -0
- data/lib/canopus/match_data_compat.rb +17 -0
- data/lib/canopus/multi_buffer.rb +266 -0
- data/lib/canopus/pane.rb +61 -0
- data/lib/canopus/patch/composite.rb +12 -0
- data/lib/canopus/patch/reload.rb +18 -0
- data/lib/canopus/patch.rb +27 -0
- data/lib/canopus/performance_recorder.rb +207 -0
- data/lib/canopus/plugins/api.rb +50 -0
- data/lib/canopus/plugins/isolated_runtime.rb +167 -0
- data/lib/canopus/plugins/local_runtime.rb +25 -0
- data/lib/canopus/plugins/permission_denied.rb +7 -0
- data/lib/canopus/plugins/registry.rb +30 -0
- data/lib/canopus/plugins.rb +11 -0
- data/lib/canopus/project/ignore_matcher.rb +104 -0
- data/lib/canopus/project/search.rb +164 -0
- data/lib/canopus/project/search_worker/cancelled.rb +3 -0
- data/lib/canopus/project/search_worker/runner.rb +9 -0
- data/lib/canopus/project/search_worker.rb +108 -0
- data/lib/canopus/project/tree.rb +48 -0
- data/lib/canopus/project/watcher.rb +59 -0
- data/lib/canopus/project.rb +127 -0
- data/lib/canopus/regexp_compat.rb +30 -0
- data/lib/canopus/save_conflict.rb +5 -0
- data/lib/canopus/selection.rb +11 -0
- data/lib/canopus/settings.rb +118 -0
- data/lib/canopus/snippet/transform.rb +209 -0
- data/lib/canopus/snippet.rb +183 -0
- data/lib/canopus/tab_map.rb +30 -0
- data/lib/canopus/terminal/cell.rb +7 -0
- data/lib/canopus/terminal/grid.rb +321 -0
- data/lib/canopus/terminal/pty.rb +103 -0
- data/lib/canopus/terminal/scrollback.rb +38 -0
- data/lib/canopus/terminal/vt.rb +398 -0
- data/lib/canopus/terminal.rb +13 -0
- data/lib/canopus/theme.rb +43 -0
- data/lib/canopus/version.rb +5 -0
- data/lib/canopus/vim/commandable.rb +148 -0
- data/lib/canopus/vim/motionable.rb +321 -0
- data/lib/canopus/vim/operator_capable.rb +377 -0
- data/lib/canopus/vim/text_object_selectable.rb +141 -0
- data/lib/canopus/vim.rb +426 -0
- data/lib/canopus/workspace/edit/executable.rb +139 -0
- data/lib/canopus/workspace/edit/node.rb +8 -0
- data/lib/canopus/workspace/edit/plan.rb +170 -0
- data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
- data/lib/canopus/workspace/edit.rb +6 -0
- data/lib/canopus/workspace/file_change_aware.rb +40 -0
- data/lib/canopus/workspace/file_previewable.rb +31 -0
- data/lib/canopus/workspace/git_aware.rb +140 -0
- data/lib/canopus/workspace/language_aware.rb +413 -0
- data/lib/canopus/workspace/language_server_configurable.rb +181 -0
- data/lib/canopus/workspace/project_searchable.rb +208 -0
- data/lib/canopus/workspace/project_tree_editable.rb +82 -0
- data/lib/canopus/workspace/session_persistable.rb +153 -0
- data/lib/canopus/workspace/settings_aware.rb +91 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
- data/lib/canopus/workspace/view.rb +643 -0
- data/lib/canopus/workspace.rb +525 -0
- data/lib/canopus/wrap_map.rb +108 -0
- data/lib/canopus.rb +24 -0
- data/sig/canopus.rbs +375 -0
- data/sig/controller.rbs +55 -0
- data/sig/display_stages.rbs +49 -0
- data/sig/git.rbs +140 -0
- data/sig/lsp.rbs +99 -0
- data/sig/markdown.rbs +11 -0
- data/sig/performance_recorder.rbs +23 -0
- data/sig/search_services.rbs +8 -0
- data/sig/services.rbs +81 -0
- data/sig/snippet.rbs +42 -0
- data/sig/terminal.rbs +125 -0
- data/sig/vim.rbs +27 -0
- data/sig/workspace_edits.rbs +11 -0
- data/sig/workspace_services.rbs +117 -0
- data/tools/certify_snippet_regex.rb +35 -0
- data/tools/check_dependencies.rb +80 -0
- data/tools/native_check.rb +70 -0
- data/tools/package.rb +113 -0
- data/tools/package_test.rb +32 -0
- 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,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
|