canopus 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
data/lib/canopus/editor.rb
CHANGED
|
@@ -4,23 +4,26 @@ require_relative "selection"
|
|
|
4
4
|
|
|
5
5
|
module Canopus
|
|
6
6
|
class Editor
|
|
7
|
-
|
|
7
|
+
DEFAULT_CLOSING_PAIRS = {"(" => ")", "[" => "]", "{" => "}", '"' => '"', "'" => "'"}.freeze
|
|
8
8
|
attr_reader :buffer, :display_map, :selections, :scroll_x, :scroll_y
|
|
9
|
-
attr_accessor :tab_size, :use_tabs, :auto_pairs, :composition, :viewport_rows, :relative_line_numbers
|
|
9
|
+
attr_accessor :tab_size, :use_tabs, :auto_pairs, :closing_pairs, :composition, :viewport_rows, :relative_line_numbers
|
|
10
10
|
|
|
11
11
|
def initialize(buffer = Buffer.new, wrap_width: nil, tab_size: 4)
|
|
12
12
|
@buffer, @next_selection = buffer, 0
|
|
13
|
-
@tab_size, @use_tabs, @auto_pairs = tab_size, false, true
|
|
13
|
+
@tab_size, @use_tabs, @auto_pairs, @closing_pairs = tab_size, false, true, DEFAULT_CLOSING_PAIRS
|
|
14
14
|
@display_map = DisplayMap.new(buffer, wrap_width: wrap_width, tab_size: tab_size)
|
|
15
15
|
@scroll_x, @scroll_y, @viewport_rows = 0, 0, 30
|
|
16
16
|
select(0)
|
|
17
17
|
@subscription = buffer.on_edit do |patch|
|
|
18
18
|
@display_map.block_map.blocks.values.each { |block| @display_map.remove_block(block.id) if block.kind == :git_diff }
|
|
19
|
+
previous = @selections
|
|
19
20
|
@selections = @selections.map { |s| Selection.new(s.id, patch.map_offset(s.anchor), patch.map_offset(s.head), s.goal) }.freeze
|
|
21
|
+
notify_selection if @selections != previous
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
def dispose
|
|
23
25
|
@subscription.detach
|
|
26
|
+
@selection_listeners&.clear
|
|
24
27
|
@display_map.dispose
|
|
25
28
|
@language_document&.dispose
|
|
26
29
|
clear_snippet
|
|
@@ -40,23 +43,30 @@ module Canopus
|
|
|
40
43
|
self
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
def on_selection(&listener)
|
|
47
|
+
raise ArgumentError, "selection listener required" unless listener
|
|
48
|
+
(@selection_listeners ||= []) << listener
|
|
49
|
+
Zaniah::Subscription.new { @selection_listeners.delete(listener) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_selections(selections, merge: true)
|
|
53
|
+
previous = @selections
|
|
54
|
+
unless merge
|
|
55
|
+
@selections = ordered_selections(selections).freeze
|
|
56
|
+
@buffer.selections = @selections
|
|
57
|
+
notify_selection if @selections != previous
|
|
58
|
+
return @selections
|
|
52
59
|
end
|
|
53
|
-
@selections =
|
|
60
|
+
@selections = merged_selections(selections).freeze
|
|
54
61
|
@buffer.selections = @selections
|
|
62
|
+
notify_selection if @selections != previous
|
|
63
|
+
@selections
|
|
55
64
|
end
|
|
56
65
|
|
|
57
|
-
def insert_text(text, auto_indent: true)
|
|
66
|
+
def insert_text(text, auto_indent: true, pair: true)
|
|
58
67
|
raise ArgumentError, "text must be UTF-8" unless text.is_a?(String) && text.valid_encoding?
|
|
59
|
-
|
|
68
|
+
closer = @closing_pairs[text] if pair && @auto_pairs
|
|
69
|
+
texts = merged_selections(@selections, touching: false).map do |selection|
|
|
60
70
|
if text == "\n" && auto_indent
|
|
61
71
|
indent = language_document.indent_for(selection.start, tab_size: @tab_size, use_tabs: @use_tabs)
|
|
62
72
|
if language_document.definition == Language::PLAIN
|
|
@@ -64,30 +74,40 @@ module Canopus
|
|
|
64
74
|
indent += @use_tabs ? "\t" : " " * @tab_size if before.rstrip.end_with?("{", "[", "(")
|
|
65
75
|
end
|
|
66
76
|
@buffer.line_ending + indent
|
|
67
|
-
elsif
|
|
68
|
-
text + @buffer.rope.byteslice(selection.range).to_s +
|
|
77
|
+
elsif closer
|
|
78
|
+
text + @buffer.rope.byteslice(selection.range).to_s + closer
|
|
69
79
|
else
|
|
70
80
|
text
|
|
71
81
|
end
|
|
72
82
|
end
|
|
73
|
-
|
|
74
|
-
replace_selections(texts, kind: :typing, group: true, cursor_back: paired ? PAIRS[text].bytesize : 0)
|
|
83
|
+
replace_selections(texts, kind: :typing, group: true, cursor_back: closer ? closer.bytesize : 0)
|
|
75
84
|
reveal_cursor
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
def replace_selections(texts, kind: :edit, group: false, cursor_back: 0, before_selections: @selections)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
selections = merged_selections(@selections, touching: false)
|
|
89
|
+
if texts.is_a?(Array)
|
|
90
|
+
unless [@selections.length, selections.length].include?(texts.length)
|
|
91
|
+
raise ArgumentError, "one text per selection required"
|
|
92
|
+
end
|
|
93
|
+
if selections.length != @selections.length
|
|
94
|
+
raise ArgumentError, "overlapping selections require the same text" unless texts.uniq.length == 1
|
|
95
|
+
texts = texts.first
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
texts = Array.new(selections.length, texts) if texts.is_a?(String)
|
|
99
|
+
raise ArgumentError, "one text per selection required" unless texts.length == selections.length
|
|
100
|
+
changes = selections.zip(texts).map { |selection, text| [selection.range, text] }
|
|
82
101
|
@buffer.selections = before_selections
|
|
83
102
|
delta = 0
|
|
84
|
-
after =
|
|
103
|
+
after = selections.zip(texts).map do |selection, text|
|
|
85
104
|
ending = selection.start + delta + text.bytesize - cursor_back
|
|
86
105
|
delta += text.bytesize - (selection.end - selection.start)
|
|
87
106
|
Selection.new(selection.id, ending, ending, nil)
|
|
88
107
|
end
|
|
108
|
+
after = merged_selections(after)
|
|
89
109
|
@buffer.edit(changes, kind: kind, selections: after, before_selections: before_selections, group: group)
|
|
90
|
-
set_selections(after)
|
|
110
|
+
set_selections(after, merge: false)
|
|
91
111
|
end
|
|
92
112
|
|
|
93
113
|
def delete_backward
|
|
@@ -108,14 +128,14 @@ module Canopus
|
|
|
108
128
|
def undo
|
|
109
129
|
clear_snippet
|
|
110
130
|
return false unless @buffer.undo
|
|
111
|
-
set_selections(@buffer.selections)
|
|
131
|
+
set_selections(@buffer.selections, merge: false)
|
|
112
132
|
reveal_cursor
|
|
113
133
|
true
|
|
114
134
|
end
|
|
115
135
|
def redo
|
|
116
136
|
clear_snippet
|
|
117
137
|
return false unless @buffer.redo
|
|
118
|
-
set_selections(@buffer.selections)
|
|
138
|
+
set_selections(@buffer.selections, merge: false)
|
|
119
139
|
reveal_cursor
|
|
120
140
|
true
|
|
121
141
|
end
|
|
@@ -166,6 +186,18 @@ module Canopus
|
|
|
166
186
|
end
|
|
167
187
|
|
|
168
188
|
def select_all = select(0, @buffer.rope.bytesize)
|
|
189
|
+
def select_matches(pattern, **options)
|
|
190
|
+
matches = search(pattern, **options)
|
|
191
|
+
return matches if matches.empty?
|
|
192
|
+
selections = matches.map do |range|
|
|
193
|
+
selection = Selection.new(@next_selection, range.begin, range.end, nil)
|
|
194
|
+
@next_selection += 1
|
|
195
|
+
selection
|
|
196
|
+
end
|
|
197
|
+
set_selections(selections, merge: false)
|
|
198
|
+
reveal_cursor
|
|
199
|
+
matches
|
|
200
|
+
end
|
|
169
201
|
def select_next_occurrence(all: false)
|
|
170
202
|
needle = @buffer.rope.byteslice(primary.range).to_s
|
|
171
203
|
if needle.empty?
|
|
@@ -186,13 +218,47 @@ module Canopus
|
|
|
186
218
|
end
|
|
187
219
|
|
|
188
220
|
def rectangle(first, last)
|
|
189
|
-
selections = (first
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
selections = BlockSelection.new(first, last).filter_map do |anchor, head|
|
|
222
|
+
next unless @display_map.row(anchor.row).kind == :text
|
|
223
|
+
selection = Selection.new(@next_selection, @display_map.to_buffer(anchor), @display_map.to_buffer(head), nil)
|
|
192
224
|
@next_selection += 1
|
|
193
|
-
|
|
225
|
+
selection
|
|
194
226
|
end
|
|
195
|
-
|
|
227
|
+
return if selections.empty?
|
|
228
|
+
set_selections(selections, merge: false)
|
|
229
|
+
end
|
|
230
|
+
def add_cursor(direction)
|
|
231
|
+
raise ArgumentError, "cursor direction must be up or down" unless %i[up down].include?(direction)
|
|
232
|
+
edge = @selections.public_send(direction == :up ? :min_by : :max_by) { |selection| @display_map.to_display(selection.head).row }
|
|
233
|
+
point = @display_map.to_display(edge.head)
|
|
234
|
+
row = point.row + (direction == :up ? -1 : 1)
|
|
235
|
+
column = edge.goal || point.column
|
|
236
|
+
offset = nil
|
|
237
|
+
loop do
|
|
238
|
+
return unless row.between?(0, @display_map.row_count - 1)
|
|
239
|
+
unless @display_map.row(row).kind == :text
|
|
240
|
+
row += direction == :up ? -1 : 1
|
|
241
|
+
next
|
|
242
|
+
end
|
|
243
|
+
offset = @display_map.to_buffer(DisplayPoint.new(row, column))
|
|
244
|
+
break unless @selections.any? { |selection| selection.empty? && selection.head == offset }
|
|
245
|
+
row += direction == :up ? -1 : 1
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
selection = Selection.new(@next_selection, offset, offset, column)
|
|
249
|
+
@next_selection += 1
|
|
250
|
+
set_selections([*@selections, selection], merge: false)
|
|
251
|
+
end
|
|
252
|
+
def add_cursors_to_selected_lines(edge)
|
|
253
|
+
raise ArgumentError, "line edge must be start or end" unless %i[start end].include?(edge)
|
|
254
|
+
selections = selected_rows.map do |row|
|
|
255
|
+
start = @buffer.rope.line_start(row)
|
|
256
|
+
offset = edge == :start ? start : start + @buffer.line(row).bytesize
|
|
257
|
+
selection = Selection.new(@next_selection, offset, offset, nil)
|
|
258
|
+
@next_selection += 1
|
|
259
|
+
selection
|
|
260
|
+
end
|
|
261
|
+
set_selections(selections, merge: false)
|
|
196
262
|
end
|
|
197
263
|
def indent(outdent: false)
|
|
198
264
|
change_lines(:indent) do |line|
|
|
@@ -353,6 +419,31 @@ module Canopus
|
|
|
353
419
|
end
|
|
354
420
|
|
|
355
421
|
private
|
|
422
|
+
def notify_selection
|
|
423
|
+
@selection_listeners&.dup&.each do |listener|
|
|
424
|
+
listener.call(@selections)
|
|
425
|
+
rescue StandardError => error
|
|
426
|
+
begin
|
|
427
|
+
warn "Canopus selection notification failed: #{error.message}"
|
|
428
|
+
rescue StandardError
|
|
429
|
+
nil
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def merged_selections(selections, touching: !snippet_active?)
|
|
435
|
+
ordered_selections(selections).each_with_object([]) do |selection, merged|
|
|
436
|
+
if merged.last && (selection.start < merged.last.end || (touching && selection.start == merged.last.end))
|
|
437
|
+
previous = merged.pop
|
|
438
|
+
merged << Selection.new(previous.id, previous.start, [previous.end, selection.end].max, selection.goal)
|
|
439
|
+
else
|
|
440
|
+
merged << selection
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
def ordered_selections(selections)
|
|
445
|
+
selections.each_with_index.sort_by { |selection, index| [selection.start, index] }.map!(&:first)
|
|
446
|
+
end
|
|
356
447
|
def horizontal(offset, direction)
|
|
357
448
|
if @buffer.rope.respond_to?(:lazy?)
|
|
358
449
|
rope = @buffer.rope
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
module Git
|
|
5
|
+
class State
|
|
6
|
+
CACHE_LIMIT = 20
|
|
7
|
+
Snapshot = Data.define(:head, :branch, :index_stamp, :entries) do
|
|
8
|
+
def identity = [head, branch, index_stamp].freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :snapshot
|
|
12
|
+
|
|
13
|
+
def initialize(repository)
|
|
14
|
+
@repository = repository
|
|
15
|
+
@repository_lock = Mutex.new
|
|
16
|
+
@diff_cache = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capture
|
|
20
|
+
synchronize do |repository|
|
|
21
|
+
3.times do
|
|
22
|
+
before = repository_identity
|
|
23
|
+
entries = repository.status.to_h do |entry|
|
|
24
|
+
[entry.path.dup.freeze, entry.code.dup.freeze]
|
|
25
|
+
end.freeze
|
|
26
|
+
after = repository_identity
|
|
27
|
+
return Snapshot.new(*after, entries) if before == after
|
|
28
|
+
end
|
|
29
|
+
raise Thuban::RefLockError, "Git HEAD or index changed while reading status"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def identity
|
|
34
|
+
synchronize do |repository|
|
|
35
|
+
[freeze_string(repository.head), freeze_string(repository.branch), index_stamp(repository)].freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def install(snapshot)
|
|
40
|
+
raise ArgumentError, "invalid Git snapshot" unless snapshot.is_a?(Snapshot)
|
|
41
|
+
|
|
42
|
+
@snapshot = snapshot
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def invalidate
|
|
46
|
+
@snapshot = nil
|
|
47
|
+
@diff_cache.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cached_diff(key) = @diff_cache[key]
|
|
51
|
+
|
|
52
|
+
def store_diff(key, diff)
|
|
53
|
+
@diff_cache.clear if @diff_cache.length >= CACHE_LIMIT && !@diff_cache.key?(key)
|
|
54
|
+
@diff_cache[key] = diff
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def synchronize(snapshot: nil, index_lock: false, &block)
|
|
58
|
+
@repository_lock.synchronize do
|
|
59
|
+
operation = lambda do
|
|
60
|
+
if snapshot && snapshot.identity != repository_identity
|
|
61
|
+
raise Thuban::RefLockError, "Git HEAD or index changed since it was displayed"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
block.call(@repository)
|
|
65
|
+
end
|
|
66
|
+
index_lock ? with_index_lock(&operation) : operation.call
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def freeze_string(value) = value&.dup&.freeze
|
|
73
|
+
|
|
74
|
+
def index_stamp(repository)
|
|
75
|
+
path = File.join(repository.git_dir, "index")
|
|
76
|
+
return unless File.file?(path)
|
|
77
|
+
|
|
78
|
+
stat = File.stat(path)
|
|
79
|
+
[stat.mtime.to_i, stat.mtime.nsec, stat.size, stat.ino].freeze
|
|
80
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def repository_identity
|
|
85
|
+
[freeze_string(@repository.head), freeze_string(@repository.branch), index_stamp(@repository)].freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_index_lock
|
|
89
|
+
path = File.join(@repository.git_dir, "index.lock")
|
|
90
|
+
lock = File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::BINARY, 0o644)
|
|
91
|
+
yield
|
|
92
|
+
rescue Errno::EEXIST
|
|
93
|
+
raise Thuban::RefLockError, "Git index is locked: #{path}"
|
|
94
|
+
ensure
|
|
95
|
+
lock&.close
|
|
96
|
+
File.unlink(path) if lock && File.exist?(path)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -8,6 +8,8 @@ module Canopus
|
|
|
8
8
|
LINE_LIMIT = 16 << 10
|
|
9
9
|
ROW_LIMIT = 256
|
|
10
10
|
CACHE_ROWS = 512
|
|
11
|
+
SELECTION_LIMIT = 256
|
|
12
|
+
SELECTION_RANGE_LIMIT = 10_000
|
|
11
13
|
Snapshot = Data.define(:rope, :version, :id, :name, :lexer, :first, :last, :syntax)
|
|
12
14
|
attr_reader :analysis_error
|
|
13
15
|
|
|
@@ -83,10 +85,38 @@ module Canopus
|
|
|
83
85
|
def diagnostics = (request(syntax: true); @syntax ? @syntax[:diagnostics] : EMPTY)
|
|
84
86
|
def fold_ranges = (request(syntax: true); @syntax ? @syntax[:folds] : EMPTY)
|
|
85
87
|
def bracket_at(offset) = (request(syntax: true); @syntax && @syntax[:brackets][offset])
|
|
88
|
+
def brackets(rows = nil)
|
|
89
|
+
request(syntax: true)
|
|
90
|
+
values = @syntax ? @syntax[:bracket_pairs] : EMPTY
|
|
91
|
+
rows ? values.select { |pair| pair.close_row >= rows.begin && pair.open_row < rows.end } : values
|
|
92
|
+
end
|
|
93
|
+
def structure_regions = (request(syntax: true); @syntax ? @syntax[:structure_regions] : EMPTY)
|
|
94
|
+
def selection_ranges(offsets)
|
|
95
|
+
unless offsets.is_a?(Array) && offsets.length.between?(1, SELECTION_LIMIT)
|
|
96
|
+
raise ArgumentError, "selection offsets must contain 1..#{SELECTION_LIMIT} entries"
|
|
97
|
+
end
|
|
98
|
+
positions = offsets.each_with_index.map do |offset, index|
|
|
99
|
+
point = @buffer.rope.point_at(offset)
|
|
100
|
+
[index, point.row, point.column, offset].freeze
|
|
101
|
+
end.freeze
|
|
102
|
+
unless @selection_positions&.map(&:last) == offsets
|
|
103
|
+
@job&.cancel
|
|
104
|
+
@job = nil
|
|
105
|
+
@selection_positions, @selection_results = positions, {}
|
|
106
|
+
@generation += 1
|
|
107
|
+
@failed = false
|
|
108
|
+
@analysis_error = nil
|
|
109
|
+
@dirty = missing_work?
|
|
110
|
+
end
|
|
111
|
+
return unless @selection_results.length == positions.length
|
|
112
|
+
|
|
113
|
+
positions.map { |index,| @selection_results.fetch(index) }.freeze
|
|
114
|
+
end
|
|
86
115
|
|
|
87
116
|
def invalidate(patch)
|
|
88
117
|
@job&.cancel
|
|
89
118
|
@job = @syntax = nil
|
|
119
|
+
@selection_positions = @selection_results = nil
|
|
90
120
|
@syntax_rows.clear
|
|
91
121
|
if patch.is_a?(Patch::Reload)
|
|
92
122
|
@tokens.clear
|
|
@@ -112,6 +142,12 @@ module Canopus
|
|
|
112
142
|
result = job.future.await(timeout: 0)
|
|
113
143
|
if @submitted_version == @buffer.version && @submitted_generation == @generation
|
|
114
144
|
result[:tokens].each { |row, tokens| @tokens[row] = tokens; @stale.delete(row) }
|
|
145
|
+
if @selection_results
|
|
146
|
+
result[:selections].each { |index, ranges| @selection_results[index] = ranges }
|
|
147
|
+
if @selection_results.values.sum(&:length) > SELECTION_RANGE_LIMIT
|
|
148
|
+
raise Error, "too many background selection ranges"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
115
151
|
while @tokens.length > CACHE_ROWS
|
|
116
152
|
@tokens.delete(@tokens.each_key.find { |row| !@rows.include?(row) })
|
|
117
153
|
end
|
|
@@ -128,15 +164,17 @@ module Canopus
|
|
|
128
164
|
@scheduler ||= Scheduler.acquire
|
|
129
165
|
# A folded viewport can contain distant source rows. Fill one missing
|
|
130
166
|
# contiguous run at a time without paint changing/cancelling its job.
|
|
131
|
-
|
|
132
|
-
missing =
|
|
167
|
+
rows = work_rows
|
|
168
|
+
missing = rows.select { |row| row_needed?(row) }
|
|
169
|
+
missing = rows if missing.empty?
|
|
133
170
|
first = last = missing.first
|
|
134
171
|
missing.drop(1).each do |row|
|
|
135
172
|
break unless row == last + 1
|
|
136
173
|
last = row
|
|
137
174
|
end
|
|
138
175
|
snapshot = Snapshot.new(@buffer.rope, @buffer.version, @id,
|
|
139
|
-
@document.definition.name, @document.definition.lexer, first, last,
|
|
176
|
+
@document.definition.name, @document.definition.lexer, first, last,
|
|
177
|
+
{syntax: !!(@want_syntax && !syntax_complete?), selections: @selection_positions}.freeze)
|
|
140
178
|
if (@job = @scheduler.submit(snapshot, prior_syntax: @syntax))
|
|
141
179
|
@submitted_version, @submitted_generation = @buffer.version, @generation
|
|
142
180
|
end
|
|
@@ -162,6 +200,7 @@ module Canopus
|
|
|
162
200
|
@provisional.clear
|
|
163
201
|
@syntax_rows.clear
|
|
164
202
|
@syntax = nil
|
|
203
|
+
@selection_positions = @selection_results = nil
|
|
165
204
|
@analysis_error = nil
|
|
166
205
|
end
|
|
167
206
|
|
|
@@ -205,10 +244,16 @@ module Canopus
|
|
|
205
244
|
last = [last, end_row].min
|
|
206
245
|
job.check!
|
|
207
246
|
source = rope.byteslice(from, to - from).to_s.freeze
|
|
247
|
+
selection_request = snapshot.syntax.is_a?(Hash) ? snapshot.syntax : {}
|
|
248
|
+
syntax = selection_request.fetch(:syntax, snapshot.syntax == true)
|
|
249
|
+
selections = Array(selection_request[:selections]).filter_map do |index, row, column, _offset|
|
|
250
|
+
[index, row, column] if row.between?(rope.point_at(from).row, end_row)
|
|
251
|
+
end
|
|
208
252
|
{"id" => snapshot.id, "version" => snapshot.version, "name" => snapshot.name,
|
|
209
253
|
"lexer" => snapshot.lexer, "source" => source, "base" => from,
|
|
210
254
|
"base_line" => rope.point_at(from).row, "first" => first, "last" => last,
|
|
211
|
-
"syntax" =>
|
|
255
|
+
"syntax" => syntax, "selections" => selections.freeze,
|
|
256
|
+
"complete" => complete, "context" => context}.freeze
|
|
212
257
|
end
|
|
213
258
|
|
|
214
259
|
def self.decode(response, prior_syntax: nil)
|
|
@@ -220,17 +265,39 @@ module Canopus
|
|
|
220
265
|
outline = raw.fetch("outline").map do |name, kind, first, last, start, finish, depth|
|
|
221
266
|
Language::Symbol.new(name.freeze, kind.to_sym, (first...last).freeze, (start...finish).freeze, depth)
|
|
222
267
|
end.freeze
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
268
|
+
bracket_pairs = raw.fetch("brackets").map do |open_first, open_last, open_row, close_first, close_last, close_row, depth|
|
|
269
|
+
Language::Bracket.new((open_first...open_last).freeze, (close_first...close_last).freeze,
|
|
270
|
+
open_row, close_row, depth).freeze
|
|
271
|
+
end.freeze
|
|
272
|
+
brackets = bracket_pairs.each_with_object({}) do |pair, result|
|
|
273
|
+
range = (pair.open_range.begin...pair.close_range.end).freeze
|
|
274
|
+
result[pair.open_range.begin] = result[pair.close_range.begin] = range
|
|
227
275
|
end
|
|
228
276
|
{outline: outline, diagnostics: raw.fetch("diagnostics").map { |first, last, message| {range: (first...last).freeze, message: message.freeze, severity: 1}.freeze }.freeze,
|
|
229
277
|
folds: raw.fetch("folds").map { |first, last| (first...last).freeze }.freeze,
|
|
230
|
-
brackets: brackets.freeze,
|
|
278
|
+
brackets: brackets.freeze, bracket_pairs: bracket_pairs,
|
|
279
|
+
structure_regions: raw.fetch("structure_regions").map { |first, last, kind| {start_line: first, end_line: last, kind: kind.to_sym}.freeze }.freeze,
|
|
280
|
+
complete: response.fetch("complete")}.freeze
|
|
231
281
|
end
|
|
232
282
|
syntax = merge_syntax(prior_syntax, syntax) if prior_syntax && syntax && !syntax[:complete]
|
|
233
|
-
|
|
283
|
+
values = response.fetch("selections", [])
|
|
284
|
+
unless values.is_a?(Array) && values.length <= SELECTION_LIMIT
|
|
285
|
+
raise Error, "invalid background selection ranges"
|
|
286
|
+
end
|
|
287
|
+
seen, total = {}, 0
|
|
288
|
+
selections = values.to_h do |index, ranges|
|
|
289
|
+
total += ranges.length if ranges.is_a?(Array)
|
|
290
|
+
unless index.is_a?(Integer) && index.between?(0, SELECTION_LIMIT - 1) && !seen.key?(index) &&
|
|
291
|
+
ranges.is_a?(Array) && total <= SELECTION_RANGE_LIMIT && ranges.all? do |range|
|
|
292
|
+
range.is_a?(Array) && range.length == 2 && range.all? { |value| value.is_a?(Integer) && value >= 0 } &&
|
|
293
|
+
range[0] <= range[1]
|
|
294
|
+
end
|
|
295
|
+
raise Error, "invalid background selection ranges"
|
|
296
|
+
end
|
|
297
|
+
seen[index] = true
|
|
298
|
+
[index, ranges.map { |first, last| (first...last).freeze }.freeze]
|
|
299
|
+
end.freeze
|
|
300
|
+
{tokens: tokens, syntax: syntax, selections: selections}.freeze
|
|
234
301
|
end
|
|
235
302
|
|
|
236
303
|
# Decoding and bounded merging happen on a preparation thread, never in
|
|
@@ -238,17 +305,29 @@ module Canopus
|
|
|
238
305
|
def self.merge_syntax(previous, current)
|
|
239
306
|
return previous if previous[:complete]
|
|
240
307
|
outlines = (previous[:outline] + current[:outline]).uniq { |item| [item.name, item.kind, item.range, item.selection] }
|
|
241
|
-
|
|
308
|
+
bracket_pairs = (previous.fetch(:bracket_pairs, []) + current.fetch(:bracket_pairs, []))
|
|
309
|
+
.uniq { |pair| [pair.open_range, pair.close_range] }.first(8192)
|
|
310
|
+
brackets = bracket_pairs.each_with_object({}) do |pair, result|
|
|
311
|
+
range = (pair.open_range.begin...pair.close_range.end).freeze
|
|
312
|
+
result[pair.open_range.begin] = result[pair.close_range.begin] = range
|
|
313
|
+
end
|
|
314
|
+
structure_regions = (previous.fetch(:structure_regions, []) + current.fetch(:structure_regions, []))
|
|
315
|
+
.uniq { |region| [region[:start_line], region[:end_line], region[:kind]] }.first(10_000)
|
|
242
316
|
{outline: outlines.sort_by { |item| item.selection.begin }.first(10_000).freeze,
|
|
243
317
|
diagnostics: (previous[:diagnostics] + current[:diagnostics]).uniq.first(1000).freeze,
|
|
244
318
|
folds: (previous[:folds] + current[:folds]).uniq.sort_by(&:begin).first(10_000).freeze,
|
|
245
|
-
brackets: brackets.
|
|
319
|
+
brackets: brackets.freeze, bracket_pairs: bracket_pairs.freeze,
|
|
320
|
+
structure_regions: structure_regions.freeze,
|
|
246
321
|
complete: false}.freeze
|
|
247
322
|
end
|
|
248
323
|
|
|
249
324
|
private
|
|
250
|
-
def row_needed?(row)
|
|
251
|
-
|
|
325
|
+
def row_needed?(row)
|
|
326
|
+
!@tokens.key?(row) || (@want_syntax && !syntax_complete? && !@syntax_rows.key?(row)) ||
|
|
327
|
+
@selection_positions&.any? { |index, selection_row,| selection_row == row && !@selection_results.key?(index) }
|
|
328
|
+
end
|
|
329
|
+
def work_rows = [*@rows, *@selection_positions&.map { |_, row,| row }].compact.uniq.sort
|
|
330
|
+
def missing_work? = work_rows.any? { |row| row_needed?(row) }
|
|
252
331
|
|
|
253
332
|
def retain_unchanged_rows(patch)
|
|
254
333
|
return if patch.edits.empty?
|
|
@@ -29,14 +29,15 @@ module Canopus
|
|
|
29
29
|
highlighter.tokens_for(row)
|
|
30
30
|
end
|
|
31
31
|
# Explicit synchronous escape hatch. Asynchronous UI paths never call this.
|
|
32
|
-
def highlighter(strategy: nil, window_context: nil)
|
|
32
|
+
def highlighter(strategy: nil, window_context: nil, max_seconds: nil)
|
|
33
33
|
return @highlighter if @highlighter
|
|
34
|
-
require "antares"
|
|
34
|
+
require "antares" unless defined?(Antares::Highlighter)
|
|
35
35
|
large = @buffer.rope.bytesize > 1 << 20
|
|
36
36
|
@highlighter = Antares::Highlighter.new(lexer: Rouge::Lexer.find(@definition.lexer).new,
|
|
37
37
|
lines: ->(row) { @buffer.rope.byteslice(@buffer.rope.line_start(row), (row + 1 < @buffer.line_count ? @buffer.rope.line_start(row + 1) : @buffer.rope.bytesize) - @buffer.rope.line_start(row)).to_s },
|
|
38
38
|
line_count: -> { @buffer.line_count }, strategy: strategy || (large ? :window : :auto),
|
|
39
|
-
window_context: window_context || (@buffer.rope.bytesize > 10 << 20 ? 0 : 500)
|
|
39
|
+
window_context: window_context || (@buffer.rope.bytesize > 10 << 20 ? 0 : 500),
|
|
40
|
+
max_seconds: max_seconds || 0.25)
|
|
40
41
|
end
|
|
41
42
|
def invalidate(patch)
|
|
42
43
|
@background&.invalidate(patch)
|
|
@@ -88,7 +89,11 @@ module Canopus
|
|
|
88
89
|
end
|
|
89
90
|
def fold_ranges
|
|
90
91
|
return @background.fold_ranges if @background
|
|
91
|
-
|
|
92
|
+
structure_regions.map do |region|
|
|
93
|
+
first = @buffer.rope.line_start(region[:start_line]) + @buffer.line(region[:start_line]).bytesize
|
|
94
|
+
last = @buffer.rope.line_start(region[:end_line]) + @buffer.line(region[:end_line]).bytesize
|
|
95
|
+
(first...last).freeze
|
|
96
|
+
end
|
|
92
97
|
end
|
|
93
98
|
def indent_for(offset, tab_size: 4, use_tabs: false)
|
|
94
99
|
row = @buffer.rope.point_at(offset).row
|
|
@@ -101,42 +106,73 @@ module Canopus
|
|
|
101
106
|
return @background.bracket_at(offset) if @background
|
|
102
107
|
bracket_ranges[offset]
|
|
103
108
|
end
|
|
109
|
+
def brackets(rows = nil)
|
|
110
|
+
rows = bracket_rows(rows) if rows
|
|
111
|
+
return @background.brackets(rows) if @background
|
|
112
|
+
values = structure_brackets
|
|
113
|
+
rows ? values.select { |pair| pair.close_row >= rows.begin && pair.open_row < rows.end } : values.dup
|
|
114
|
+
end
|
|
115
|
+
def structure_regions
|
|
116
|
+
return @background.structure_regions if @background
|
|
117
|
+
highlighter.structure.fold_regions.map do |region|
|
|
118
|
+
{start_line: region.start_line, end_line: region.end_line, kind: region.kind}.freeze
|
|
119
|
+
end.freeze
|
|
120
|
+
end
|
|
121
|
+
def selection_ranges(offsets)
|
|
122
|
+
return @background.selection_ranges(offsets) if @background
|
|
123
|
+
offsets.map do |offset|
|
|
124
|
+
point = @buffer.rope.point_at(offset)
|
|
125
|
+
ranges = highlighter.structure.selection_ranges(point.row, point.column).map do |region|
|
|
126
|
+
first = @buffer.rope.offset_at(Denebola::Point.new(region.start_line, region.start_column || 0))
|
|
127
|
+
last = @buffer.rope.offset_at(Denebola::Point.new(region.end_line,
|
|
128
|
+
region.end_column || @buffer.line(region.end_line).length))
|
|
129
|
+
(first...last).freeze
|
|
130
|
+
end.uniq.sort_by { |range| [range.size, -range.begin] }
|
|
131
|
+
ranges.each_with_object([]) do |range, nested|
|
|
132
|
+
nested << range if !nested.last || (range.begin <= nested.last.begin && nested.last.end <= range.end)
|
|
133
|
+
end.freeze
|
|
134
|
+
end.freeze
|
|
135
|
+
end
|
|
104
136
|
|
|
105
137
|
private
|
|
138
|
+
def bracket_rows(rows)
|
|
139
|
+
unless rows.is_a?(Range) && rows.begin.is_a?(Integer) && rows.end.is_a?(Integer) && rows.begin >= 0
|
|
140
|
+
raise ArgumentError, "bracket rows must be a nonnegative integer range"
|
|
141
|
+
end
|
|
142
|
+
last = rows.exclude_end? ? rows.end : rows.end + 1
|
|
143
|
+
raise RangeError, "bracket rows outside document" if last < rows.begin || last > @buffer.line_count
|
|
144
|
+
rows.begin...last
|
|
145
|
+
end
|
|
146
|
+
|
|
106
147
|
def bracket_ranges
|
|
107
|
-
return @
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
next unless %w[PARENTHESIS_LEFT PARENTHESIS_RIGHT BRACKET_LEFT BRACKET_RIGHT BRACE_LEFT BRACE_RIGHT].include?(token.type.to_s)
|
|
113
|
-
[token.location.start_offset, token.value]
|
|
114
|
-
end
|
|
115
|
-
else
|
|
116
|
-
require "rouge"
|
|
117
|
-
collected, byte = [], 0
|
|
118
|
-
Rouge::Lexer.find(@definition.lexer).new.lex(@buffer.text).each do |token, value|
|
|
119
|
-
unless token.qualname.start_with?("Literal.String", "Comment")
|
|
120
|
-
value.b.scan(/[()\[\]{}]/n) { |character| collected << [byte + Regexp.last_match.begin(0), character] }
|
|
121
|
-
end
|
|
122
|
-
byte += value.bytesize
|
|
123
|
-
end
|
|
124
|
-
collected
|
|
148
|
+
return @bracket_ranges if @bracket_ranges_version == @buffer.version
|
|
149
|
+
|
|
150
|
+
@bracket_ranges = structure_brackets.each_with_object({}) do |pair, ranges|
|
|
151
|
+
range = (pair.open_range.begin...pair.close_range.end).freeze
|
|
152
|
+
ranges[pair.open_range.begin] = ranges[pair.close_range.begin] = range
|
|
125
153
|
end
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
@bracket_ranges_version = @buffer.version
|
|
155
|
+
@bracket_ranges
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def structure_brackets
|
|
159
|
+
return @structure_brackets if @structure_brackets_version == @buffer.version
|
|
160
|
+
|
|
161
|
+
boundaries = {}
|
|
162
|
+
byte_offset = lambda do |row, column|
|
|
163
|
+
positions = boundaries[row] ||= @buffer.line(row).each_char.each_with_object([0]) do |character, values|
|
|
164
|
+
values << values.last + character.bytesize
|
|
136
165
|
end
|
|
166
|
+
@buffer.rope.line_start(row) + positions.fetch(column)
|
|
137
167
|
end
|
|
138
|
-
@
|
|
139
|
-
|
|
168
|
+
@structure_brackets = highlighter.structure.brackets.map do |pair|
|
|
169
|
+
opening = byte_offset.call(pair.open_line, pair.open_column)
|
|
170
|
+
closing = byte_offset.call(pair.close_line, pair.close_column)
|
|
171
|
+
Bracket.new((opening...opening + 1).freeze, (closing...closing + 1).freeze,
|
|
172
|
+
pair.open_line, pair.close_line, pair.depth).freeze
|
|
173
|
+
end.freeze
|
|
174
|
+
@structure_brackets_version = @buffer.version
|
|
175
|
+
@structure_brackets
|
|
140
176
|
end
|
|
141
177
|
end
|
|
142
178
|
end
|