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,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
|