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,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ module Vim::Motionable
5
+ private
6
+ def source
7
+ if @source_version != @editor.buffer.version
8
+ @source_version = @editor.buffer.version
9
+ @source = @editor.buffer.text
10
+ @characters = @source.grapheme_clusters
11
+ total = 0
12
+ @offsets = [0] + @characters.map { |character| total += character.bytesize }
13
+ end
14
+ @source
15
+ end
16
+
17
+ def character_index(offset)
18
+ source
19
+ @offsets.bsearch_index { |value| value >= offset } || @characters.length
20
+ end
21
+ def character_at(offset)
22
+ index = character_index(offset)
23
+ @characters[index] || ""
24
+ end
25
+ def row_at(offset) = @editor.buffer.rope.point_at(offset.clamp(0, @editor.buffer.rope.bytesize)).row
26
+ def line_start(row) = @editor.buffer.rope.line_start(row.clamp(0, @editor.buffer.line_count - 1))
27
+ def line_end(row) = line_start(row) + @editor.buffer.line(row).bytesize
28
+ def last_row
29
+ count = @editor.buffer.line_count
30
+ count > 1 && @editor.buffer.line(count - 1).empty? ? count - 2 : count - 1
31
+ end
32
+ def next_offset(offset)
33
+ index = character_index(offset)
34
+ @offsets[[index + 1, @characters.length].min]
35
+ end
36
+ def previous_offset(offset)
37
+ index = character_index(offset)
38
+ @offsets[[index - 1, 0].max]
39
+ end
40
+ def normal_offset(offset)
41
+ row = [row_at(offset), last_row].min
42
+ first, ending = line_start(row), line_end(row)
43
+ return first if first == ending
44
+ offset.clamp(first, previous_offset(ending))
45
+ end
46
+ def first_nonblank(row)
47
+ line_start(row) + @editor.buffer.line(row)[/\A[ \t]*/].bytesize
48
+ end
49
+ def horizontal(offset, amount, insertion: false)
50
+ index = character_index(offset)
51
+ row = row_at(offset)
52
+ target = @offsets[(index + amount).clamp(0, @characters.length)]
53
+ first, ending = line_start(row), line_end(row)
54
+ ending = previous_offset(ending) if !insertion && ending > first
55
+ target.clamp(first, ending)
56
+ end
57
+
58
+ def column_at(offset)
59
+ column = 0
60
+ @editor.buffer.rope.byteslice(line_start(row_at(offset))...offset).to_s.each_grapheme_cluster do |character|
61
+ column += cell_width(character, column)
62
+ end
63
+ column
64
+ end
65
+ def offset_at(row, column, insertion: false)
66
+ offset, current = line_start(row), 0
67
+ @editor.buffer.line(row).each_grapheme_cluster do |character|
68
+ break if current >= column
69
+ width = cell_width(character, current)
70
+ break if current + width > column
71
+ current += width
72
+ offset += character.bytesize
73
+ end
74
+ insertion ? offset : normal_offset(offset)
75
+ end
76
+
77
+ def move_to(offset)
78
+ if visual?
79
+ @visual_head = normal_offset(offset)
80
+ update_visual
81
+ else
82
+ @editor.select(normal_offset(offset))
83
+ end
84
+ @editor.reveal_cursor
85
+ end
86
+
87
+ def move(key, count, explicit: false)
88
+ if (motion = motion_target(key, count, explicit: explicit))
89
+ move_to(motion.first)
90
+ end
91
+ end
92
+
93
+ def motion_target(key, count, explicit: false)
94
+ offset, row = cursor_position, row_at(cursor_position)
95
+ inclusive, linewise = false, false
96
+ vertical = %w[j k up down ctrl-f ctrl-b ctrl-d ctrl-u].include?(key)
97
+ @goal_column = nil unless vertical
98
+ target = case key
99
+ when "h", "left", "l", "right"
100
+ amount = %w[h left].include?(key) ? -count : count
101
+ horizontal(offset, amount, insertion: !!@operator)
102
+ when "0" then line_start(row)
103
+ when "^" then first_nonblank(row)
104
+ when "$"
105
+ inclusive = true
106
+ ending = line_end([row + count - 1, last_row].min)
107
+ ending > line_start([row + count - 1, last_row].min) ? previous_offset(ending) : ending
108
+ when "j", "k", "up", "down", "ctrl-f", "ctrl-b", "ctrl-d", "ctrl-u"
109
+ amount = case key
110
+ when "ctrl-f", "ctrl-b" then [@editor.viewport_rows - 2, 1].max
111
+ when "ctrl-d", "ctrl-u" then [@editor.viewport_rows / 2, 1].max
112
+ else 1
113
+ end
114
+ amount *= -1 if %w[k up ctrl-b ctrl-u].include?(key)
115
+ @goal_column ||= column_at(offset)
116
+ linewise = true
117
+ destination = (row + amount * count).clamp(0, last_row)
118
+ return if destination == row
119
+ offset_at(destination, @goal_column)
120
+ when "G", "gg"
121
+ linewise = true
122
+ destination = key == "G" && !explicit ? last_row : count - 1
123
+ first_nonblank(destination.clamp(0, last_row))
124
+ when "w", "W", "b", "B", "e", "E", "ge", "gE", "ce", "cE"
125
+ return if offset.zero? && %w[b B ge gE].include?(key)
126
+ inclusive = %w[e E ge gE ce cE].include?(key)
127
+ word_target(offset, key, count)
128
+ when "{", "}"
129
+ destination = row
130
+ count.times do
131
+ step = key == "}" ? 1 : -1
132
+ destination += step
133
+ destination += step while destination.between?(1, last_row - 1) && !@editor.buffer.line(destination).strip.empty?
134
+ destination = destination.clamp(0, last_row)
135
+ end
136
+ line_start(destination)
137
+ when "%"
138
+ if explicit
139
+ linewise = true
140
+ first_nonblank(((last_row + 1) * count / 100.0).ceil.clamp(1, last_row + 1) - 1)
141
+ else
142
+ inclusive = true
143
+ bracket_target(offset)
144
+ end
145
+ when ";", ","
146
+ return unless @last_find
147
+ char, kind = @last_find
148
+ kind = {"f" => "F", "F" => "f", "t" => "T", "T" => "t"}.fetch(kind) if key == ","
149
+ inclusive = %w[f t].include?(kind)
150
+ find_character(char, kind, count, repeated: true)
151
+ else return
152
+ end
153
+ target && [target, {inclusive: inclusive, linewise: linewise}]
154
+ end
155
+
156
+ def word_class(character, big)
157
+ return :space if !character || character.match?(/\s/)
158
+ big || character.match?(/[\p{Alnum}_]/) ? :word : :punctuation
159
+ end
160
+ def word_target(offset, kind, count)
161
+ index = character_index(offset)
162
+ big = kind.include?("W") || kind.include?("B") || kind.include?("E")
163
+ count.times do |iteration|
164
+ case kind.downcase.delete_prefix("c")
165
+ when "w"
166
+ origin = index
167
+ initial = word_class(@characters[index], big)
168
+ index += 1 while index < @characters.length && word_class(@characters[index], big) == initial && !@characters[index].include?("\n")
169
+ while index < @characters.length && word_class(@characters[index], big) == :space
170
+ break if index > origin && @characters[index].include?("\n") && @characters[index - 1].include?("\n")
171
+ index += 1
172
+ end
173
+ when "b"
174
+ index = [index - 1, 0].max
175
+ while index.positive? && word_class(@characters[index], big) == :space
176
+ break if @characters[index].include?("\n") && @characters[index - 1].include?("\n")
177
+ index -= 1
178
+ end
179
+ initial = word_class(@characters[index], big)
180
+ index -= 1 while initial != :space && index.positive? && word_class(@characters[index - 1], big) == initial
181
+ when "e"
182
+ index += 1 if index < @characters.length - 1 && !(kind.start_with?("c") && iteration.zero?)
183
+ index += 1 while index < @characters.length - 1 && word_class(@characters[index], big) == :space
184
+ initial = word_class(@characters[index], big)
185
+ index += 1 while index < @characters.length - 1 && word_class(@characters[index + 1], big) == initial
186
+ when "ge"
187
+ initial = word_class(@characters[index], big)
188
+ index -= 1 while index.positive? && word_class(@characters[index - 1], big) == initial
189
+ index = [index - 1, 0].max
190
+ index -= 1 while index.positive? && word_class(@characters[index], big) == :space
191
+ end
192
+ end
193
+ @offsets[[index, @characters.length].min]
194
+ end
195
+
196
+ def find_character(char, kind, count, repeated: false)
197
+ return unless char.grapheme_clusters.length == 1
198
+ offset = cursor_position
199
+ row = row_at(offset)
200
+ line = @editor.buffer.line(row).grapheme_clusters
201
+ position = @editor.buffer.rope.byteslice(line_start(row)...offset).to_s.grapheme_clusters.length
202
+ direction = %w[F T].include?(kind) ? -1 : 1
203
+ position += direction if repeated && %w[t T].include?(kind)
204
+ count.times do
205
+ position += direction
206
+ position += direction while position.between?(0, line.length - 1) && line[position] != char
207
+ return unless position.between?(0, line.length - 1)
208
+ end
209
+ position -= direction if %w[t T].include?(kind)
210
+ line_start(row) + line.take(position).join.bytesize
211
+ end
212
+
213
+ def bracket_target(offset)
214
+ source
215
+ index = character_index(offset)
216
+ pairs = {"(" => ")", "[" => "]", "{" => "}"}
217
+ index += 1 while index < @characters.length && !@characters[index].include?("\n") && !"()[]{}".include?(@characters[index])
218
+ return if index >= @characters.length || @characters[index].include?("\n")
219
+ character = @characters[index]
220
+ direction = pairs.key?(character) ? 1 : -1
221
+ partner = direction == 1 ? pairs[character] : pairs.key(character)
222
+ depth = 1
223
+ while (index += direction).between?(0, @characters.length - 1)
224
+ depth += 1 if @characters[index] == character
225
+ depth -= 1 if @characters[index] == partner
226
+ return @offsets[index] if depth.zero?
227
+ end
228
+ nil
229
+ end
230
+
231
+ def line_range(count)
232
+ row = row_at(cursor_position)
233
+ line_start(row)...([row + count, last_row + 1].min < @editor.buffer.line_count ? line_start([row + count, last_row + 1].min) : @editor.buffer.rope.bytesize)
234
+ end
235
+
236
+ def visual_ranges
237
+ first, last = [@visual_anchor, @visual_head].minmax
238
+ if @mode == :visual_line
239
+ first_row, last = row_at(first), row_at(last)
240
+ ending = last + 1 < @editor.buffer.line_count ? line_start(last + 1) : @editor.buffer.rope.bytesize
241
+ [line_start(first_row)...ending]
242
+ elsif @mode == :visual_block
243
+ columns = block_columns
244
+ (row_at(first)..row_at(last)).map do |row|
245
+ beginning = offset_at(row, columns[0], insertion: true)
246
+ ending = offset_at(row, columns[1] + 1, insertion: true)
247
+ beginning...ending
248
+ end
249
+ else
250
+ [first...next_offset(last)]
251
+ end
252
+ end
253
+
254
+ def update_visual
255
+ ranges = visual_ranges
256
+ selections = ranges.each_with_index.map { |range, i| Selection.new(i, range.begin, range.end, nil) }
257
+ @editor.set_selections(selections)
258
+ end
259
+
260
+ def cell_width(character, column)
261
+ return @editor.tab_size - column % @editor.tab_size if character == "\t"
262
+ defined?(Terminal::Grid) ? [Terminal::Grid.width(character), 1].max : 1
263
+ end
264
+
265
+ def block_columns
266
+ columns = [@visual_anchor, @visual_head].map { |offset| column_at(offset) }
267
+ endings = [@visual_anchor, @visual_head].zip(columns).map { |offset, column| column + cell_width(character_at(offset), column) - 1 }
268
+ [columns.min, endings.max]
269
+ end
270
+
271
+ # A rectangular edge can bisect a tab or a double-width glyph. Keep the
272
+ # unselected cells as spaces while operating on the selected cell span.
273
+ def block_slices
274
+ left, right = block_columns
275
+ right += 1
276
+ first, last = [row_at(@visual_anchor), row_at(@visual_head)].minmax
277
+ (first..last).map do |row|
278
+ offset, column, begin_offset, end_offset = line_start(row), 0, nil, nil
279
+ selected, prefix, suffix, cells = +"", +"", +"", 0
280
+ @editor.buffer.line(row).each_grapheme_cluster do |character|
281
+ width = cell_width(character, column)
282
+ if column < right && column + width > left
283
+ begin_offset ||= offset
284
+ end_offset = offset + character.bytesize
285
+ overlap = [column + width, right].min - [column, left].max
286
+ cells += overlap
287
+ selected << (overlap == width ? character : " " * overlap)
288
+ prefix = " " * (left - column) if column < left
289
+ suffix = " " * (column + width - right) if column + width > right
290
+ end
291
+ offset += character.bytesize
292
+ column += width
293
+ end
294
+ {range: (begin_offset || offset)...(end_offset || offset), text: selected, prefix: prefix, suffix: suffix, row: row, width: cells}
295
+ end
296
+ end
297
+
298
+ # Split by screen cells, not bytes; only a tab intersected by the insertion
299
+ # edge is expanded. Vim pads before a bisected wide glyph without deleting it.
300
+ def split_at_column(line, target, replace_wide: false, pad_wide: true)
301
+ offset, column = 0, 0
302
+ line.each_grapheme_cluster do |character|
303
+ break if column >= target
304
+ width = cell_width(character, column)
305
+ if column + width > target
306
+ padding = character == "\t" || pad_wide ? target - column : 0
307
+ prefix = line.byteslice(0, offset) + " " * padding
308
+ suffix = if character == "\t" || replace_wide
309
+ " " * (column + width - target) + line.byteslice(offset + character.bytesize..)
310
+ else
311
+ line.byteslice(offset..)
312
+ end
313
+ return [prefix, suffix]
314
+ end
315
+ offset += character.bytesize
316
+ column += width
317
+ end
318
+ [line.byteslice(0, offset) + " " * [target - column, 0].max, line.byteslice(offset..)]
319
+ end
320
+ end
321
+ end