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,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Canopus::DisplayMap::Worker
|
|
4
|
+
BATCH_LINES = 32
|
|
5
|
+
QUEUE_LIMIT = 8
|
|
6
|
+
Job = Data.define(:generation, :tree, :builder, :prefix)
|
|
7
|
+
Result = Data.define(:generation, :first, :lines, :error)
|
|
8
|
+
Cancelled = Class.new(StandardError)
|
|
9
|
+
private_constant :Cancelled
|
|
10
|
+
attr_reader :thread, :results
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@requests, @results = Queue.new, SizedQueue.new(QUEUE_LIMIT)
|
|
14
|
+
@thread = Thread.new { run }
|
|
15
|
+
@thread.name = "canopus-wrap"
|
|
16
|
+
@thread.report_on_exception = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def submit(generation, tree, builder, prefix)
|
|
20
|
+
@generation = generation
|
|
21
|
+
@requests.clear
|
|
22
|
+
@results.clear
|
|
23
|
+
@requests << Job.new(generation, tree, builder, prefix)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
return if @closed
|
|
28
|
+
@closed = true
|
|
29
|
+
@generation = nil
|
|
30
|
+
@requests.close
|
|
31
|
+
@results.close
|
|
32
|
+
# Cancellation checkpoints normally finish in milliseconds. No native
|
|
33
|
+
# resource or shared mutable document state belongs to this worker.
|
|
34
|
+
@thread.kill unless @thread.join(1)
|
|
35
|
+
@thread.join(0.1)
|
|
36
|
+
@results.clear
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def run
|
|
42
|
+
while (job = @requests.pop)
|
|
43
|
+
yielded_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
checkpoint = lambda do
|
|
45
|
+
raise Cancelled if @closed || @generation != job.generation
|
|
46
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - yielded_at >= 0.002
|
|
47
|
+
Thread.pass
|
|
48
|
+
yielded_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
begin
|
|
52
|
+
first, lines, bytes = nil, [], 0
|
|
53
|
+
flush = lambda do
|
|
54
|
+
unless lines.empty?
|
|
55
|
+
checkpoint.call
|
|
56
|
+
@results << Result.new(job.generation, first, lines.freeze, nil)
|
|
57
|
+
first, lines, bytes = nil, [], 0
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
source_rows(job) do |row|
|
|
61
|
+
checkpoint.call
|
|
62
|
+
flush.call if first && row != first + lines.length
|
|
63
|
+
first ||= row
|
|
64
|
+
value = job.builder.line(row, checkpoint: checkpoint)
|
|
65
|
+
lines << value
|
|
66
|
+
bytes += value.rows.sum { |line| line.text.bytesize + line.offsets.length * 8 }
|
|
67
|
+
flush.call if lines.length >= BATCH_LINES || bytes >= 65_536
|
|
68
|
+
end
|
|
69
|
+
flush.call
|
|
70
|
+
rescue Cancelled
|
|
71
|
+
next
|
|
72
|
+
rescue ClosedQueueError
|
|
73
|
+
break if @closed
|
|
74
|
+
rescue StandardError => error
|
|
75
|
+
@results << Result.new(job.generation, 0, [].freeze, error) unless @closed
|
|
76
|
+
ensure
|
|
77
|
+
job = checkpoint = flush = lines = value = nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
rescue ClosedQueueError
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def source_rows(job)
|
|
85
|
+
count = job.tree.prefix_summary(job.prefix).pending_rows
|
|
86
|
+
count.times { |index| yield job.tree.locate(index, :pending_rows)[0] }
|
|
87
|
+
(job.prefix...job.tree.size).each { |row| yield row }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fold_map"
|
|
4
|
+
require_relative "tab_map"
|
|
5
|
+
require_relative "wrap_map"
|
|
6
|
+
require_relative "block_map"
|
|
7
|
+
require_relative "display_point"
|
|
8
|
+
|
|
9
|
+
module Canopus
|
|
10
|
+
class DisplayMap
|
|
11
|
+
BACKGROUND_THRESHOLD = 1 << 20
|
|
12
|
+
Row = Data.define(:text, :offsets, :kind, :metadata)
|
|
13
|
+
attr_reader :fold_map, :tab_map, :wrap_map, :block_map, :tree, :recomputed_lines
|
|
14
|
+
attr_reader :layout_error
|
|
15
|
+
|
|
16
|
+
def initialize(buffer, tab_size: 4, wrap_width: nil, background_threshold: BACKGROUND_THRESHOLD)
|
|
17
|
+
raise ArgumentError, "background threshold must be nonnegative or nil" unless background_threshold.nil? || (background_threshold.is_a?(Integer) && background_threshold >= 0)
|
|
18
|
+
@background_threshold, @generation = background_threshold, 0
|
|
19
|
+
@buffer, @rope = buffer, buffer.rope
|
|
20
|
+
@fold_map, @tab_map, @wrap_map, @block_map = FoldMap.new, TabMap.new(tab_size: tab_size), WrapMap.new(width: wrap_width), BlockMap.new
|
|
21
|
+
rebuild
|
|
22
|
+
@subscription = buffer.on_edit { |patch| apply(patch) }
|
|
23
|
+
end
|
|
24
|
+
def dispose
|
|
25
|
+
return if @disposed
|
|
26
|
+
@disposed = true
|
|
27
|
+
@layout_error = nil
|
|
28
|
+
@subscription.detach
|
|
29
|
+
stop_wrap(@wrap_map, @worker)
|
|
30
|
+
@worker = @pending_result = nil
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
def pending? = !!(!@disposed && !@failed && !@lazy && @background && (@computed_prefix < @tree.size || @tree.summary.pending_rows.positive?))
|
|
34
|
+
def progress
|
|
35
|
+
return [@rope.line_count, @rope.line_count] if @lazy
|
|
36
|
+
[@computed_prefix - @tree.prefix_summary(@computed_prefix).pending_rows, @tree.size]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Call only from the foreground event loop, before painting. Read methods
|
|
40
|
+
# deliberately never install completed work halfway through a frame.
|
|
41
|
+
def poll(max_lines: 256)
|
|
42
|
+
raise ArgumentError, "max_lines must be positive" unless max_lines.is_a?(Integer) && max_lines.positive?
|
|
43
|
+
return false if @disposed || !@worker
|
|
44
|
+
changed, consumed = false, 0
|
|
45
|
+
while consumed < max_lines
|
|
46
|
+
result = @pending_result || @worker.results.pop(true)
|
|
47
|
+
@pending_result = nil
|
|
48
|
+
next unless result.generation == @generation
|
|
49
|
+
if result.error
|
|
50
|
+
@failed = true
|
|
51
|
+
@layout_error = result.error
|
|
52
|
+
@worker.close
|
|
53
|
+
@worker = nil
|
|
54
|
+
raise result.error
|
|
55
|
+
end
|
|
56
|
+
take = [max_lines - consumed, result.lines.length].min
|
|
57
|
+
values = result.lines.take(take)
|
|
58
|
+
replace_values(result.first, take, values)
|
|
59
|
+
@computed_prefix = [@computed_prefix, result.first + take].max if result.first <= @computed_prefix
|
|
60
|
+
take.times { |index| @row_cache.delete(result.first + index) }
|
|
61
|
+
@recomputed_lines += take
|
|
62
|
+
consumed += take
|
|
63
|
+
changed = true
|
|
64
|
+
if take < result.lines.length
|
|
65
|
+
@pending_result = result.with(first: result.first + take, lines: result.lines.drop(take).freeze)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
changed
|
|
69
|
+
rescue ThreadError
|
|
70
|
+
changed
|
|
71
|
+
end
|
|
72
|
+
def row_count = @lazy ? @rope.line_count : @tree.summary.display_rows
|
|
73
|
+
def wrap_width=(width)
|
|
74
|
+
raise Error, "soft wrapping is disabled for large read-only files" if @lazy && width
|
|
75
|
+
return if width == @wrap_map.width && !@wrap_map.font
|
|
76
|
+
replace_wrap(WrapMap.new(width: width))
|
|
77
|
+
end
|
|
78
|
+
def wrap_pixels(width, font: nil, font_size:, font_paths: nil, typesetter: nil)
|
|
79
|
+
raise Error, "soft wrapping is disabled for large read-only files" if @lazy
|
|
80
|
+
current = @wrap_map
|
|
81
|
+
return if current.width == width && current.font.equal?(font || typesetter&.font) && current.font_size == font_size && current.font_paths == font_paths && current.typesetter.equal?(typesetter)
|
|
82
|
+
replace_wrap(WrapMap.new(width: width, font: font, font_size: font_size, font_paths: font_paths, typesetter: typesetter))
|
|
83
|
+
end
|
|
84
|
+
def tab_size=(size)
|
|
85
|
+
return if size == @tab_map.tab_size
|
|
86
|
+
@tab_map = TabMap.new(tab_size: size)
|
|
87
|
+
rebuild(reuse: true)
|
|
88
|
+
end
|
|
89
|
+
def fold(range)
|
|
90
|
+
raise Error, "folding is disabled for large read-only files" if @lazy
|
|
91
|
+
range = range.begin...(range.end + (range.exclude_end? ? 0 : 1))
|
|
92
|
+
@rope.point_at(range.begin)
|
|
93
|
+
@rope.point_at(range.end)
|
|
94
|
+
first, last = affected_lines(@rope.point_at(range.begin).row, @rope.point_at(range.end).row)
|
|
95
|
+
@fold_map.fold(range)
|
|
96
|
+
replace_lines(first, last, last)
|
|
97
|
+
end
|
|
98
|
+
def unfold(offset)
|
|
99
|
+
affected = @fold_map.ranges.select { |range| range.cover?(offset) }
|
|
100
|
+
return if affected.empty?
|
|
101
|
+
first = @rope.point_at(affected.map(&:begin).min).row
|
|
102
|
+
last = @rope.point_at(affected.map(&:end).max).row
|
|
103
|
+
first, last = affected_lines(first, last)
|
|
104
|
+
@fold_map.unfold(offset)
|
|
105
|
+
replace_lines(first, last, last)
|
|
106
|
+
end
|
|
107
|
+
def insert_block(id, row:, text:, kind: :diagnostic)
|
|
108
|
+
raise Error, "inline blocks are disabled for large read-only files" if @lazy
|
|
109
|
+
raise RangeError, "block row outside buffer" unless row.between?(0, @rope.line_count - 1)
|
|
110
|
+
previous = @block_map.blocks[id]
|
|
111
|
+
@block_map.insert(id, row: row, text: text, kind: kind)
|
|
112
|
+
replace_lines(previous.row, previous.row, previous.row) if previous && previous.row != row
|
|
113
|
+
replace_lines(row, row, row)
|
|
114
|
+
end
|
|
115
|
+
def remove_block(id)
|
|
116
|
+
block = @block_map.remove(id)
|
|
117
|
+
replace_lines(block.row, block.row, block.row) if block
|
|
118
|
+
end
|
|
119
|
+
def row(index)
|
|
120
|
+
if @lazy
|
|
121
|
+
raise RangeError, "display row outside buffer" unless index.between?(0, row_count - 1)
|
|
122
|
+
@row_cache.shift if @row_cache.length >= 256
|
|
123
|
+
return @row_cache[index] ||= build_lazy_row(index)
|
|
124
|
+
end
|
|
125
|
+
source, _lines, prefix = locate(index)
|
|
126
|
+
lines = display_lines(source)
|
|
127
|
+
lines.rows[index - prefix.display_rows]
|
|
128
|
+
end
|
|
129
|
+
def source_row(index) = @lazy ? index : locate(index)[0]
|
|
130
|
+
def each_row(range = 0...row_count)
|
|
131
|
+
return enum_for(__method__, range) unless block_given?
|
|
132
|
+
range.each { |i| yield row(i), i if i.between?(0, row_count - 1) }
|
|
133
|
+
end
|
|
134
|
+
def to_buffer(point, bias: :left)
|
|
135
|
+
if @lazy
|
|
136
|
+
source = point.row.clamp(0, row_count - 1)
|
|
137
|
+
line = row(source)
|
|
138
|
+
return @rope.line_start(source) + line.offsets[point.column.clamp(0, line.offsets.length - 1)]
|
|
139
|
+
end
|
|
140
|
+
row = point.row.clamp(0, row_count - 1)
|
|
141
|
+
source, _lines, prefix = locate(row)
|
|
142
|
+
lines = display_lines(source)
|
|
143
|
+
line = lines.rows[row - prefix.display_rows]
|
|
144
|
+
column = point.column.clamp(0, line.offsets.length - 1)
|
|
145
|
+
local = line.offsets[column]
|
|
146
|
+
@rope.line_start(source) + local
|
|
147
|
+
end
|
|
148
|
+
def to_display(offset, bias: :left)
|
|
149
|
+
if @lazy
|
|
150
|
+
point = @rope.point_at(offset)
|
|
151
|
+
local = offset - @rope.line_start(point.row)
|
|
152
|
+
line = row(point.row)
|
|
153
|
+
unless local.between?(line.offsets.first, line.offsets.last)
|
|
154
|
+
line = @row_cache[point.row] = build_lazy_row(point.row, from: [local - 8192, 0].max)
|
|
155
|
+
end
|
|
156
|
+
column = line.offsets.bsearch_index { |position| position >= local } || line.text.length
|
|
157
|
+
return DisplayPoint.new(point.row, column)
|
|
158
|
+
end
|
|
159
|
+
source = @rope.point_at(offset).row
|
|
160
|
+
loop do
|
|
161
|
+
start = @rope.line_start(source)
|
|
162
|
+
folded = @fold_map.ranges.find { |range| range.begin < start && range.end >= start }
|
|
163
|
+
break unless folded
|
|
164
|
+
source = @rope.point_at(folded.begin).row
|
|
165
|
+
end
|
|
166
|
+
local = offset - @rope.line_start(source)
|
|
167
|
+
lines = display_lines(source, local: local).rows
|
|
168
|
+
prefix = @tree.prefix_summary(source).display_rows
|
|
169
|
+
lines.each_with_index do |line, i|
|
|
170
|
+
next unless line.kind == :text
|
|
171
|
+
next if local > line.offsets.last
|
|
172
|
+
column = line.offsets.bsearch_index { |n| bias == :left ? n >= local : n > local }
|
|
173
|
+
column = column ? (bias == :right ? [column - 1, 0].max : column) : line.offsets.length - 1
|
|
174
|
+
return DisplayPoint.new(prefix + i, column)
|
|
175
|
+
end
|
|
176
|
+
DisplayPoint.new(prefix + lines.length - 1, lines.last&.text&.length || 0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
def replace_wrap(wrap)
|
|
181
|
+
# A copy may own native resources. Cancel/join before closing its providers,
|
|
182
|
+
# then transfer the new immutable typography snapshot to the next worker.
|
|
183
|
+
if @wrap_map.font
|
|
184
|
+
stop_wrap(@wrap_map, @worker)
|
|
185
|
+
@worker = @pending_result = nil
|
|
186
|
+
else
|
|
187
|
+
@wrap_map.close
|
|
188
|
+
end
|
|
189
|
+
@wrap_map = wrap
|
|
190
|
+
rebuild(reuse: true)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def stop_wrap(wrap, worker)
|
|
194
|
+
worker&.close
|
|
195
|
+
if worker&.thread&.alive?
|
|
196
|
+
# An uninterruptible custom native call can outlive Thread#kill. Never
|
|
197
|
+
# release its providers until it really stops. Default Ruby layout needs
|
|
198
|
+
# no reaper thread; native providers must keep their calls bounded.
|
|
199
|
+
Thread.new { worker.thread.join; wrap.close }.name = "canopus-layout-cleanup"
|
|
200
|
+
else
|
|
201
|
+
wrap.close
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def affected_lines(first, last)
|
|
206
|
+
ranges = @fold_map.ranges.map { |range| [@rope.point_at(range.begin).row, @rope.point_at(range.end).row] }
|
|
207
|
+
loop do
|
|
208
|
+
previous = [first, last]
|
|
209
|
+
ranges.each do |from, to|
|
|
210
|
+
next if to < first || from > last
|
|
211
|
+
first, last = [first, from].min, [last, to].max
|
|
212
|
+
end
|
|
213
|
+
return [first, last] if previous == [first, last]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def display_lines(source, local: nil)
|
|
218
|
+
value = @tree[source]
|
|
219
|
+
return value unless value.is_a?(PendingLineSet)
|
|
220
|
+
cached = @row_cache[source]
|
|
221
|
+
if cached && local
|
|
222
|
+
positions = cached.rows.first.offsets
|
|
223
|
+
cached = nil unless local.between?(positions.first, positions.last)
|
|
224
|
+
end
|
|
225
|
+
unless cached
|
|
226
|
+
@row_cache.shift if @row_cache.length >= 256
|
|
227
|
+
cached = @row_cache[source] = @builder.line(source, provisional: true, from: local ? [local - 8192, 0].max : 0)
|
|
228
|
+
end
|
|
229
|
+
cached
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def build_lazy_row(row, from: 0)
|
|
233
|
+
@recomputed_lines += 1
|
|
234
|
+
text, offset = @rope.line_window(row, from: from)
|
|
235
|
+
offsets = [offset]
|
|
236
|
+
text.each_char { |char| offset += char.bytesize; offsets << offset }
|
|
237
|
+
value, positions = @tab_map.transform(text, offsets)
|
|
238
|
+
Row.new(value.freeze, positions.freeze, :text, nil)
|
|
239
|
+
end
|
|
240
|
+
def locate(index)
|
|
241
|
+
raise RangeError, "display row outside buffer" unless index.between?(0, row_count - 1)
|
|
242
|
+
return @location_cache[index] if @location_cache.key?(index)
|
|
243
|
+
@location_cache.shift if @location_cache.length >= 256
|
|
244
|
+
@location_cache[index] = @tree.locate(index, :display_rows)
|
|
245
|
+
end
|
|
246
|
+
def build_line(row)
|
|
247
|
+
@recomputed_lines += 1
|
|
248
|
+
@builder.line(row)
|
|
249
|
+
end
|
|
250
|
+
def rebuild(reuse: false)
|
|
251
|
+
@recomputed_lines = 0
|
|
252
|
+
@row_cache = {}
|
|
253
|
+
@location_cache = {}
|
|
254
|
+
@pending_result = nil
|
|
255
|
+
@generation += 1
|
|
256
|
+
@failed = false
|
|
257
|
+
@layout_error = nil
|
|
258
|
+
@lazy = @rope.respond_to?(:lazy?) && @rope.lazy?
|
|
259
|
+
if @lazy
|
|
260
|
+
@worker&.close
|
|
261
|
+
@worker = nil
|
|
262
|
+
return
|
|
263
|
+
end
|
|
264
|
+
@builder = LineBuilder.new(@rope, @fold_map, @tab_map, @wrap_map, @block_map)
|
|
265
|
+
@background = background_layout?(@rope)
|
|
266
|
+
unless reuse && @background && @tree && @tree.size == @rope.line_count
|
|
267
|
+
values = @background ? @builder.pending_lines(0, @rope.line_count) : Array.new(@rope.line_count) { |row| build_line(row) }
|
|
268
|
+
@tree = Denebola::Tree.new(values, summary: Summary)
|
|
269
|
+
end
|
|
270
|
+
@computed_prefix = @background ? 0 : @tree.size
|
|
271
|
+
schedule
|
|
272
|
+
end
|
|
273
|
+
def replace_lines(first, old_last, new_last)
|
|
274
|
+
@generation += 1
|
|
275
|
+
@row_cache = {}
|
|
276
|
+
@pending_result = nil
|
|
277
|
+
@failed = false
|
|
278
|
+
@layout_error = nil
|
|
279
|
+
@builder = LineBuilder.new(@rope, @fold_map, @tab_map, @wrap_map, @block_map)
|
|
280
|
+
replacement = @background ? @builder.pending_lines(first, new_last - first + 1) : Array.new(new_last - first + 1) { |i| build_line(first + i) }
|
|
281
|
+
if first < @computed_prefix
|
|
282
|
+
@computed_prefix = old_last + 1 >= @computed_prefix ? first : @computed_prefix + new_last - old_last
|
|
283
|
+
end
|
|
284
|
+
replace_values(first, old_last - first + 1, replacement)
|
|
285
|
+
@computed_prefix = @tree.size unless @background
|
|
286
|
+
schedule
|
|
287
|
+
end
|
|
288
|
+
def replace_values(first, count, replacement)
|
|
289
|
+
tail = first + count
|
|
290
|
+
@tree = @tree.slice(0, first).append(replacement).append(@tree.slice(tail, @tree.size - tail))
|
|
291
|
+
@location_cache.clear
|
|
292
|
+
end
|
|
293
|
+
def schedule
|
|
294
|
+
if pending?
|
|
295
|
+
@worker ||= Worker.new
|
|
296
|
+
@worker.submit(@generation, @tree, @builder, @computed_prefix)
|
|
297
|
+
else
|
|
298
|
+
@worker&.close
|
|
299
|
+
@worker = nil
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
def background_layout?(rope)
|
|
303
|
+
@background_threshold && (@wrap_map.font || rope.bytesize >= @background_threshold)
|
|
304
|
+
end
|
|
305
|
+
def apply(patch)
|
|
306
|
+
if patch.is_a?(Patch::Reload)
|
|
307
|
+
@rope = patch.after
|
|
308
|
+
rebuild
|
|
309
|
+
return
|
|
310
|
+
end
|
|
311
|
+
if patch.is_a?(Patch::Composite)
|
|
312
|
+
patch.patches.each { |part| apply(part) }
|
|
313
|
+
return
|
|
314
|
+
end
|
|
315
|
+
if patch.edits.empty?
|
|
316
|
+
@rope = patch.after
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
if @lazy || !!background_layout?(@rope) != !!background_layout?(patch.after)
|
|
320
|
+
@fold_map.apply(patch)
|
|
321
|
+
@block_map.apply(patch)
|
|
322
|
+
@rope = patch.after
|
|
323
|
+
rebuild
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
@recomputed_lines = 0
|
|
327
|
+
first = patch.edits.map { |e| patch.before.point_at(e.old_range.begin).row }.min
|
|
328
|
+
old_last = patch.edits.map { |e| patch.before.point_at(e.old_range.end).row }.max
|
|
329
|
+
new_last = patch.edits.map { |e| patch.after.point_at(e.new_range.end).row }.max
|
|
330
|
+
first, old_last = affected_lines(first, old_last)
|
|
331
|
+
new_last = [new_last, patch.after.point_at(patch.map_offset(patch.before.line_start(old_last))).row].max
|
|
332
|
+
@fold_map.apply(patch)
|
|
333
|
+
@block_map.apply(patch)
|
|
334
|
+
@rope = patch.after
|
|
335
|
+
replace_lines(first, old_last, new_last)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
require_relative "display_map/summary"
|
|
341
|
+
require_relative "display_map/line_set"
|
|
342
|
+
require_relative "display_map/pending_line_set"
|
|
343
|
+
|
|
344
|
+
Canopus::DisplayMap::EMPTY_LINES = Canopus::DisplayMap::LineSet.new([].freeze)
|
|
345
|
+
Canopus::DisplayMap::UNWRAPPED_LINE = Canopus::DisplayMap::PendingLineSet.new(1)
|
|
346
|
+
Canopus::DisplayMap.send(:private_constant, :LineSet, :PendingLineSet, :EMPTY_LINES, :UNWRAPPED_LINE)
|
|
347
|
+
|
|
348
|
+
require_relative "display_map/line_builder"
|
|
349
|
+
require_relative "display_map/worker"
|
|
350
|
+
Canopus::DisplayMap.send(:private_constant, :LineBuilder, :Worker)
|