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,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ module Vim::OperatorCapable
5
+ private
6
+
7
+ CONTROL_KEYS = {"esc" => "\e", "enter" => "\r", "tab" => "\t", "backspace" => "\b", "ctrl-v" => "\x16", "ctrl-r" => "\x12"}.freeze
8
+ def register_keys(text) = text.grapheme_clusters.map { |key| CONTROL_KEYS.key(key) || key }
9
+ def save_macro_register(name) = @registers[name] = [@macros[name].map { |key| CONTROL_KEYS.fetch(key, key) }.join, false]
10
+ def register_entry(name)
11
+ case name
12
+ when "%" then [@editor.buffer.path.to_s, false]
13
+ when "/" then [@search.to_s, false]
14
+ when "_" then ["", false]
15
+ else @registers[name&.downcase]
16
+ end
17
+ end
18
+
19
+ def store_register(text, type, operator, width: nil)
20
+ return if @register == "_"
21
+ entry = width ? [text, type, width] : [text, type]
22
+ if @register.match?(/\A[A-Z]\z/)
23
+ name = @register.downcase
24
+ old = @registers[name]
25
+ entry = [(old ? old.first : "") + text, old && old[1] == true ? true : type]
26
+ @registers[name] = entry
27
+ elsif @register != '"'
28
+ @registers[@register] = entry
29
+ end
30
+ if operator == "y"
31
+ @registers["0"] = entry if @register == '"'
32
+ elsif type == true || text.include?("\n")
33
+ 9.downto(2) { |index| @registers[index.to_s] = @registers[(index - 1).to_s] if @registers[(index - 1).to_s] }
34
+ @registers["1"] = entry
35
+ else
36
+ @registers["-"] = entry
37
+ end
38
+ @registers['"'] = entry
39
+ end
40
+
41
+ def normalized_line_range(range)
42
+ first, last = row_at(range.begin), row_at(range.end)
43
+ last -= 1 if range.end > range.begin && last > first && range.end == line_start(last)
44
+ ending = last + 1 < @editor.buffer.line_count ? line_start(last + 1) : @editor.buffer.rope.bytesize
45
+ line_start(first)...ending
46
+ end
47
+
48
+ def operate(operator, range, linewise: false, destination: nil, shift: 1)
49
+ return unless operator && range
50
+ original = cursor_position
51
+ before = [Selection.new(@editor.primary.id, original, original, nil)]
52
+ range = normalized_line_range(range) if linewise
53
+ text = @editor.buffer.rope.byteslice(range).to_s
54
+ if %w[d c y].include?(operator)
55
+ registered = linewise && !text.end_with?("\n") ? text + @editor.buffer.line_ending : text
56
+ store_register(registered, linewise, operator)
57
+ end
58
+ start_group if operator == "c"
59
+ selection = range
60
+ if operator == "d" && linewise && range.end == @editor.buffer.rope.bytesize && range.begin.positive? && !source.end_with?("\n")
61
+ selection = previous_offset(range.begin)...range.end
62
+ end
63
+ @mode = :normal
64
+ @editor.select(selection.begin, selection.end)
65
+ case operator
66
+ when "d"
67
+ @editor.replace_selections("", kind: :vim, before_selections: before) unless selection.begin == selection.end
68
+ @editor.select(first_nonblank([row_at([selection.begin, @editor.buffer.rope.bytesize].min), last_row].min)) if linewise
69
+ when "c"
70
+ indentation = linewise ? text[/\A[ \t]*/] : ""
71
+ replacement = indentation + (linewise && (range.end < @editor.buffer.rope.bytesize || text.end_with?("\n")) ? @editor.buffer.line_ending : "")
72
+ @editor.replace_selections(replacement, kind: :vim, before_selections: before)
73
+ @editor.select(range.begin + indentation.bytesize)
74
+ when "y" then @editor.select(linewise ? original : range.begin)
75
+ when ">", "<"
76
+ indent_rows(range, outdent: operator == "<", before: before, amount: shift)
77
+ @editor.select(first_nonblank(row_at([range.begin, @editor.buffer.rope.bytesize].min)))
78
+ when "=" then reindent(range, before: before)
79
+ when "gu", "gU", "g~"
80
+ transformed = text.public_send({"gu" => :downcase, "gU" => :upcase, "g~" => :swapcase}.fetch(operator))
81
+ @editor.replace_selections(transformed, kind: :vim, before_selections: before)
82
+ @editor.select(range.begin)
83
+ end
84
+ @operator, @register = nil, '"'
85
+ if operator == "c"
86
+ @change_keys, @insert_count, @replace_stack = @command.dup, 1, []
87
+ @insert_prefix_length = @change_keys.length
88
+ @mode = :insert
89
+ @command.clear
90
+ else
91
+ @editor.select(normal_offset(destination || @editor.primary.head))
92
+ operator == "y" ? @command.clear : finish_change
93
+ end
94
+ end
95
+
96
+ def operate_visual(operator, count: 1)
97
+ @last_visual = [@mode, @visual_anchor, @visual_head]
98
+ @command = @visual_keys.dup if @visual_keys
99
+ ranges = visual_ranges
100
+ return operate(operator, ranges.first, linewise: @mode == :visual_line, destination: operator == "y" ? ranges.first.begin : nil, shift: count) unless @mode == :visual_block
101
+ first = ranges.first.begin
102
+ width = block_columns[1] - block_columns[0] + 1
103
+ slices = block_slices
104
+ ranges, texts = slices.map { |slice| slice[:range] }, slices.map { |slice| slice[:text] }
105
+ store_register(texts.join("\n"), :block, operator, width: width) if %w[d c y].include?(operator)
106
+ start_group if operator == "c"
107
+ @editor.set_selections(ranges.each_with_index.map { |range, index| Selection.new(index, range.begin, range.end, nil) })
108
+ case operator
109
+ when "d", "c"
110
+ @editor.replace_selections(slices.map { |slice| slice[:prefix] + slice[:suffix] }, kind: :vim)
111
+ delta = 0
112
+ cursors = slices.each_with_index.map do |slice, index|
113
+ range = slice[:range]
114
+ offset = range.begin + delta + slice[:prefix].bytesize
115
+ delta += slice[:prefix].bytesize + slice[:suffix].bytesize - (range.end - range.begin)
116
+ Selection.new(index, offset, offset, nil)
117
+ end
118
+ @editor.set_selections(cursors)
119
+ first = cursors.first.head
120
+ when "gu", "gU", "g~"
121
+ method = {"gu" => :downcase, "gU" => :upcase, "g~" => :swapcase}.fetch(operator)
122
+ @editor.replace_selections(slices.map { |slice| slice[:prefix] + slice[:text].public_send(method) + slice[:suffix] }, kind: :vim)
123
+ when ">", "<"
124
+ unit = @editor.use_tabs ? "\t" * count : " " * (@editor.tab_size * count)
125
+ edits = slices.map do |slice|
126
+ row = slice[:row]
127
+ line = @editor.buffer.line(row)
128
+ prefix, suffix = split_at_column(line, block_columns[0], replace_wide: operator == "<", pad_wide: operator != ">")
129
+ if operator == ">" && !@editor.use_tabs
130
+ whitespace = suffix[/\A[ \t]*/]
131
+ column = block_columns[0]
132
+ whitespace.each_char { |char| column += cell_width(char, column) }
133
+ suffix = " " * (column - block_columns[0]) + suffix.delete_prefix(whitespace)
134
+ end
135
+ value = operator == ">" ? unit + suffix : suffix.sub(/\A(?:\t{1,#{count}}| {1,#{@editor.tab_size * count}})/, "")
136
+ [line_start(row)...line_end(row), prefix + value]
137
+ end
138
+ @editor.buffer.edit(edits, kind: :vim)
139
+ when "=" then reindent(ranges.first.begin...ranges.last.end)
140
+ end
141
+ @operator, @register = nil, '"'
142
+ if operator == "c"
143
+ @change_keys, @insert_count, @replace_stack = @command.dup, 1, []
144
+ @insert_prefix_length = @change_keys.length
145
+ @mode = :insert
146
+ @block_insert_start = @editor.buffer.anchor(first, bias: :left)
147
+ @block_insert_keep_start = false
148
+ @command.clear
149
+ else
150
+ @mode = :normal
151
+ @editor.select(normal_offset(first))
152
+ operator == "y" ? @command.clear : finish_change
153
+ end
154
+ end
155
+
156
+ def paste(after, count)
157
+ entry = register_entry(@register)
158
+ return @command.clear unless entry
159
+ text, type, width = entry
160
+ if visual?
161
+ paste_visual(entry, count)
162
+ elsif type == :block
163
+ paste_block(text, after, count, width)
164
+ else
165
+ row = row_at(cursor_position)
166
+ if type == true
167
+ if after && row == last_row && !source.end_with?("\n")
168
+ first = @editor.buffer.rope.bytesize
169
+ inserted = @editor.buffer.line_ending + (text * count).delete_suffix(@editor.buffer.line_ending)
170
+ destination = first + @editor.buffer.line_ending.bytesize
171
+ else
172
+ first = after ? (row + 1 < @editor.buffer.line_count ? line_start(row + 1) : @editor.buffer.rope.bytesize) : line_start(row)
173
+ inserted, destination = text * count, first
174
+ end
175
+ else
176
+ first = after ? horizontal(cursor_position, 1, insertion: true) : cursor_position
177
+ inserted = text * count
178
+ destination = inserted.include?("\n") ? first : first + inserted.bytesize - (inserted.grapheme_clusters.last&.bytesize || 0)
179
+ end
180
+ @editor.select(first)
181
+ @editor.replace_selections(inserted, kind: :vim)
182
+ @editor.select(normal_offset(destination))
183
+ end
184
+ @register = '"'
185
+ finish_change
186
+ end
187
+
188
+ def enter_block_insert(key, count)
189
+ @last_visual = [@mode, @visual_anchor, @visual_head]
190
+ @change_keys, @insert_count, @replace_stack = @visual_keys.dup, count, []
191
+ @insert_prefix_length = @change_keys.length
192
+ columns = block_columns
193
+ column = key == "I" ? columns[0] : columns[1] + 1
194
+ ranges = visual_ranges
195
+ rows = ranges.filter_map do |range|
196
+ row = row_at(range.begin)
197
+ finish = line_end(row)
198
+ next if key == "I" && column_at(finish) < column
199
+ row
200
+ end
201
+ edits = rows.map do |row|
202
+ prefix, suffix = split_at_column(@editor.buffer.line(row), column)
203
+ [line_start(row)...line_end(row), prefix + suffix]
204
+ end
205
+ @editor.buffer.edit(edits, kind: :vim_insert) unless edits.empty?
206
+ points = rows.map { |row| offset_at(row, column, insertion: true) }
207
+ points = [ranges.first.begin] if points.empty?
208
+ @editor.set_selections(points.each_with_index.map { |offset, i| Selection.new(i, offset, offset, nil) })
209
+ @block_insert_start = @editor.buffer.anchor(offset_at(rows.first || row_at(ranges.first.begin), columns[0], insertion: true), bias: :left)
210
+ @block_insert_keep_start = true
211
+ @mode = :insert
212
+ @command.clear
213
+ end
214
+
215
+ def replace_visual(key)
216
+ return unless key.grapheme_clusters.length == 1
217
+ ranges = visual_ranges
218
+ @last_visual = [@mode, @visual_anchor, @visual_head]
219
+ @command = @visual_keys.dup
220
+ @editor.set_selections(ranges.each_with_index.map { |range, i| Selection.new(i, range.begin, range.end, nil) })
221
+ if @mode == :visual_block
222
+ slices = block_slices
223
+ ranges = slices.map { |slice| slice[:range] }
224
+ @editor.set_selections(ranges.each_with_index.map { |range, i| Selection.new(i, range.begin, range.end, nil) })
225
+ texts = slices.map do |slice|
226
+ slice[:prefix] + key * slice[:width] + slice[:suffix]
227
+ end
228
+ else
229
+ texts = ranges.map do |range|
230
+ @editor.buffer.rope.byteslice(range).to_s.grapheme_clusters.map { |char| char.include?("\n") ? char : key }.join
231
+ end
232
+ end
233
+ @editor.replace_selections(texts, kind: :vim)
234
+ @mode = :normal
235
+ @editor.select(normal_offset(ranges.first.begin))
236
+ finish_change
237
+ end
238
+
239
+ def paste_visual(entry, count)
240
+ text, type, width = entry
241
+ mode, ranges = @mode, visual_ranges
242
+ slices = block_slices if mode == :visual_block
243
+ ranges = slices.map { |slice| slice[:range] } if slices
244
+ first = ranges.first.begin
245
+ @last_visual = [@mode, @visual_anchor, @visual_head]
246
+ @command = @visual_keys.dup
247
+ removed = slices ? slices.map { |slice| slice[:text] } : ranges.map { |range| @editor.buffer.rope.byteslice(range).to_s }
248
+ original_eol = source.end_with?("\n")
249
+ @register = '"'
250
+ store_register(removed.join(mode == :visual_block ? "\n" : ""), mode == :visual_line ? true : (mode == :visual_block ? :block : false), "d",
251
+ width: mode == :visual_block ? block_columns[1] - block_columns[0] + 1 : nil)
252
+ @mode = :normal
253
+ @editor.buffer.begin_undo_group
254
+ begin
255
+ @editor.set_selections(ranges.each_with_index.map { |range, i| Selection.new(i, range.begin, range.end, nil) })
256
+ if mode == :visual_block
257
+ if type == :block || text.include?("\n")
258
+ @editor.replace_selections(slices.map { |slice| slice[:prefix] + slice[:suffix] }, kind: :vim)
259
+ @editor.select(first + slices.first[:prefix].bytesize)
260
+ paste_block(text, false, count, width)
261
+ else
262
+ @editor.replace_selections(slices.map { |slice| slice[:prefix] + text * count + slice[:suffix] }, kind: :vim)
263
+ @editor.select(normal_offset(first + slices.first[:prefix].bytesize))
264
+ end
265
+ else
266
+ replacement = text * count
267
+ if mode == :visual_line
268
+ replacement += @editor.buffer.line_ending unless replacement.end_with?("\n")
269
+ replacement = replacement.delete_suffix(@editor.buffer.line_ending) if ranges.first.end == @editor.buffer.rope.bytesize && !original_eol
270
+ destination = first
271
+ elsif type == true
272
+ replacement = @editor.buffer.line_ending + replacement
273
+ destination = first + @editor.buffer.line_ending.bytesize
274
+ else
275
+ destination = first + replacement.bytesize - (replacement.grapheme_clusters.last&.bytesize || 0)
276
+ end
277
+ @editor.replace_selections(replacement, kind: :vim)
278
+ @editor.select(normal_offset(destination))
279
+ end
280
+ ensure
281
+ @editor.buffer.end_undo_group
282
+ end
283
+ end
284
+
285
+ def paste_block(text, after, count, width)
286
+ row, column = row_at(cursor_position), column_at(cursor_position)
287
+ column += cell_width(character_at(cursor_position), column) if after
288
+ lines = source.split(@editor.buffer.line_ending, -1)
289
+ trailing = lines.last == "" && lines.length > 1
290
+ lines.pop if trailing
291
+ chunks = text.split("\n", -1)
292
+ first = nil
293
+ chunks.each_with_index do |chunk, i|
294
+ destination = row + i
295
+ lines << "" while lines.length <= destination
296
+ line = lines[destination]
297
+ prefix, suffix = split_at_column(line, column)
298
+ cells = 0
299
+ chunk.each_grapheme_cluster { |char| cells += cell_width(char, column + cells) }
300
+ value = (chunk + " " * [(width || cells) - cells, 0].max) * count
301
+ lines[destination] = prefix + value + suffix
302
+ first ||= lines.take(destination).sum { |part| part.bytesize + @editor.buffer.line_ending.bytesize } + prefix.bytesize
303
+ end
304
+ replacement = lines.join(@editor.buffer.line_ending) + (trailing ? @editor.buffer.line_ending : "")
305
+ @editor.select_all
306
+ @editor.replace_selections(replacement, kind: :vim)
307
+ @editor.select(normal_offset(first || 0))
308
+ end
309
+
310
+ def join_lines(count)
311
+ row = row_at(cursor_position)
312
+ last = [row + [count - 1, 1].max, last_row].min
313
+ return @command.clear if last == row
314
+ first = line_end(row)
315
+ text = @editor.buffer.line(row)
316
+ (row + 1..last).each do |index|
317
+ first = line_start(row) + text.bytesize
318
+ following = @editor.buffer.line(index).lstrip
319
+ text += " " unless text.empty? || text.end_with?(" ", "\t") || following.empty? || following.start_with?(")")
320
+ text += following
321
+ end
322
+ @editor.select(line_start(row), line_end(last))
323
+ @editor.replace_selections(text, kind: :vim)
324
+ @editor.select(normal_offset(first))
325
+ finish_change
326
+ end
327
+
328
+ def reindent(range, before: @editor.selections)
329
+ selection = normalized_line_range(range)
330
+ first = row_at(selection.begin)
331
+ last = row_at(selection.end)
332
+ last -= 1 if last > first && selection.end == line_start(last)
333
+ definition = @editor.language_document.definition
334
+ level, previous, replacements, indent_columns = 0, 0, [], [0]
335
+ significant_indentation = %w[python yaml].include?(definition.name)
336
+ (0..last).each do |row|
337
+ line = @editor.buffer.line(row)
338
+ content = line.lstrip
339
+ closing = definition.indent_close.match?(content) && !content.empty?
340
+ level = [level - 1, 0].max if closing
341
+ if significant_indentation && !content.empty?
342
+ column = 0
343
+ line[/\A[ \t]*/].each_char { |char| column += cell_width(char, column) }
344
+ indent_columns.pop while indent_columns.length > 1 && column < indent_columns.last
345
+ indent_columns << column if column > indent_columns.last
346
+ end
347
+ depth = if significant_indentation
348
+ indent_columns.length - 1
349
+ else
350
+ definition == Language::PLAIN ? previous : level
351
+ end
352
+ if row >= first
353
+ indent = content.empty? ? "" : (@editor.use_tabs ? "\t" * depth : " " * (@editor.tab_size * depth))
354
+ replacements << [line_start(row)...line_end(row), indent + content]
355
+ end
356
+ previous = row >= first ? depth : line[/\A[ \t]*/].gsub("\t", " " * @editor.tab_size).length / @editor.tab_size unless content.empty?
357
+ level += 1 if definition.indent_open.match?(content) || (definition.name == "ruby" && content.match?(/\A(?:else|elsif|rescue|ensure)\b/))
358
+ end
359
+ caret = Selection.new(0, selection.begin, selection.begin, nil)
360
+ @editor.buffer.edit(replacements, kind: :vim, before_selections: before, selections: [caret])
361
+ @editor.select(normal_offset(first_nonblank(first)))
362
+ end
363
+
364
+ def indent_rows(range, outdent: false, before: @editor.selections, amount: 1)
365
+ selection = normalized_line_range(range)
366
+ first, last = row_at(selection.begin), row_at(selection.end)
367
+ last -= 1 if last > first && selection.end == line_start(last)
368
+ edits = (first..last).filter_map do |row|
369
+ line = @editor.buffer.line(row)
370
+ next if line.empty?
371
+ result = outdent ? line.sub(/\A(?:\t{1,#{amount}}| {1,#{@editor.tab_size * amount}})/, "") : (@editor.use_tabs ? "\t" * amount : " " * (@editor.tab_size * amount)) + line
372
+ [line_start(row)...line_end(row), result]
373
+ end
374
+ @editor.buffer.edit(edits, kind: :vim, before_selections: before) unless edits.empty?
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ module Vim::TextObjectSelectable
5
+ private
6
+
7
+ def text_object(key, inner:, count: 1)
8
+ source
9
+ position = character_index(cursor_position)
10
+ case key
11
+ when "w", "W"
12
+ return if @characters.empty?
13
+ big = key == "W"
14
+ first = last = [position, @characters.length - 1].min
15
+ initial = word_class(@characters[first], big)
16
+ first -= 1 while first.positive? && word_class(@characters[first - 1], big) == initial && !@characters[first - 1].include?("\n")
17
+ last += 1 while last < @characters.length && word_class(@characters[last], big) == initial && !@characters[last].include?("\n")
18
+ (count - 1).times do
19
+ last += 1 while !inner && last < @characters.length && word_class(@characters[last], big) == :space
20
+ klass = word_class(@characters[last], big)
21
+ last += 1 while last < @characters.length && word_class(@characters[last], big) == klass
22
+ end
23
+ unless inner
24
+ if initial == :space
25
+ klass = word_class(@characters[last], big)
26
+ last += 1 while last < @characters.length && word_class(@characters[last], big) == klass
27
+ else
28
+ trailing = last
29
+ last += 1 while last < @characters.length && @characters[last].match?(/[ \t]/)
30
+ first -= 1 while last == trailing && first.positive? && @characters[first - 1].match?(/[ \t]/)
31
+ end
32
+ end
33
+ @offsets[first]...@offsets[last]
34
+ when "p" then paragraph_object(inner, count)
35
+ when "s" then sentence_object(inner, count)
36
+ when "t" then tag_object(position, inner, count)
37
+ when '"', "'", "`"
38
+ row = row_at(cursor_position)
39
+ start_index, end_index = character_index(line_start(row)), character_index(line_end(row))
40
+ quotes = (start_index...end_index).select { |index| @characters[index] == key && !escaped_character?(index) }
41
+ pairs = quotes.each_slice(2).select { |pair| pair.length == 2 }
42
+ pair = pairs.find { |first, last| first <= position && position <= last } || pairs.find { |first, _| first > position }
43
+ return unless pair
44
+ first, last = pair
45
+ if inner
46
+ first += 1
47
+ else
48
+ trailing = last + 1
49
+ last += 1 while last + 1 < end_index && @characters[last + 1].match?(/[ \t]/)
50
+ first -= 1 while last + 1 == trailing && first > start_index && @characters[first - 1].match?(/[ \t]/)
51
+ end
52
+ @offsets[first]...@offsets[inner ? pair.last : last + 1]
53
+ else
54
+ left, right = {"(" => ["(", ")"], ")" => ["(", ")"], "b" => ["(", ")"],
55
+ "[" => ["[", "]"], "]" => ["[", "]"], "{" => ["{", "}"], "}" => ["{", "}"],
56
+ "B" => ["{", "}"], "<" => ["<", ">"], ">" => ["<", ">"]}[key]
57
+ return unless left
58
+ stack, enclosing = [], []
59
+ @characters.each_with_index do |character, index|
60
+ stack << index if character == left
61
+ if character == right && !stack.empty?
62
+ first = stack.pop
63
+ enclosing << [first, index] if first <= position && index >= position
64
+ end
65
+ end
66
+ first, last = enclosing[count - 1]
67
+ return unless first
68
+ first += 1 if inner
69
+ last += 1 unless inner
70
+ @offsets[first]...@offsets[last]
71
+ end
72
+ end
73
+
74
+ def escaped_character?(index)
75
+ slashes = 0
76
+ while index.positive? && @characters[index - 1] == "\\"
77
+ slashes += 1
78
+ index -= 1
79
+ end
80
+ slashes.odd?
81
+ end
82
+
83
+ def paragraph_object(inner, count)
84
+ first = last = row_at(cursor_position)
85
+ blank = @editor.buffer.line(first).strip.empty?
86
+ first -= 1 while first.positive? && @editor.buffer.line(first - 1).strip.empty? == blank
87
+ last += 1 while last < last_row && @editor.buffer.line(last + 1).strip.empty? == blank
88
+ (count - 1).times do
89
+ last += 1 while last < last_row && @editor.buffer.line(last + 1).strip.empty?
90
+ last += 1 while last < last_row && !@editor.buffer.line(last + 1).strip.empty?
91
+ end
92
+ unless inner
93
+ ending = last
94
+ last += 1 while last < last_row && @editor.buffer.line(last + 1).strip.empty? != blank
95
+ first -= 1 while last == ending && first.positive? && @editor.buffer.line(first - 1).strip.empty?
96
+ end
97
+ line_start(first)...(last + 1 < @editor.buffer.line_count ? line_start(last + 1) : @editor.buffer.rope.bytesize)
98
+ end
99
+
100
+ def sentence_object(inner, count)
101
+ positions, first = [], 0
102
+ source.to_enum(:scan, /[.!?]["')\]]*(?=\s|\z)|\n[ \t]*\n/).each do
103
+ match = Regexp.last_match
104
+ ending = source[0...match.end(0)].bytesize
105
+ positions << (first...ending)
106
+ first = ending
107
+ end
108
+ positions << (first...source.bytesize) if first < source.bytesize
109
+ index = positions.index { |range| range.end > cursor_position }
110
+ return unless index
111
+ first = positions[index].begin
112
+ last = positions[[index + count - 1, positions.length - 1].min].end
113
+ first += source.byteslice(first...last)[/\A\s*/].bytesize
114
+ if inner
115
+ last -= source.byteslice(first...last)[/\s*\z/].bytesize
116
+ else
117
+ last += source.byteslice(last..)[/\A\s*/].bytesize
118
+ end
119
+ first...last
120
+ end
121
+
122
+ def tag_object(position, inner, count)
123
+ stack, enclosing = [], []
124
+ source.to_enum(:scan, /<\/?([\w:-]+)\b(?:"[^"]*"|'[^']*'|[^'">])*>/).each do
125
+ match = Regexp.last_match
126
+ first, last = source[0...match.begin(0)].bytesize, source[0...match.end(0)].bytesize
127
+ if match[0].start_with?("</")
128
+ opening = stack.rindex { |item| item[0] == match[1] }
129
+ next unless opening
130
+ _tag, left, content = stack[opening]
131
+ stack.slice!(opening..)
132
+ enclosing << [left, content, first, last] if left <= @offsets[position] && last > @offsets[position]
133
+ elsif !match[0].end_with?("/>")
134
+ stack << [match[1], first, last]
135
+ end
136
+ end
137
+ entry = enclosing[count - 1]
138
+ entry && (inner ? entry[1]...entry[2] : entry[0]...entry[3])
139
+ end
140
+ end
141
+ end