Class: Canopus::MultiBuffer
- Inherits:
-
Buffer
- Object
- Buffer
- Canopus::MultiBuffer
show all
- Defined in:
- lib/canopus/multi_buffer.rb,
sig/services.rbs
Overview
Editable, nonoverlapping source excerpts with protected headings.
Defined Under Namespace
Classes: Excerpt
Instance Attribute Summary collapse
Attributes inherited from Buffer
#bom, #detection, #disk_digest, #encoding, #history, #line_ending, #newline, #notification_errors, #path, #read_only, #rope, #selections, #version
Instance Method Summary
collapse
Methods inherited from Buffer
#anchor, #convert_line_endings, #copy_anchor, decode_bytes, #ensure_final_newline, #line, #line_count, #mixed_line_endings?, #on_edit, open, #persistent_undo_record, #release_anchor, #reload, #relocate, #resolve, #restore_persistent_undo!, #roundtrip?, #text, #trim_trailing_whitespace
Constructor Details
#initialize(excerpts: []) ⇒ MultiBuffer
Returns a new instance of MultiBuffer.
8
9
10
11
12
13
14
15
16
|
# File 'lib/canopus/multi_buffer.rb', line 8
def initialize(excerpts: [])
super("")
@excerpts, @source_subscriptions, @multi_undo, @multi_redo = [], {}, [], []
validate_excerpt_ranges(excerpts)
@building = true
excerpts.each { |buffer, range, title| add_excerpt(buffer, range, title: title) }
@building = false
refresh
end
|
Instance Attribute Details
#excerpts ⇒ Array[Excerpt]
Returns the value of attribute excerpts.
7
8
9
|
# File 'lib/canopus/multi_buffer.rb', line 7
def excerpts
@excerpts
end
|
Instance Method Details
#add_excerpt(buffer, range, title: nil) ⇒ self
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# File 'lib/canopus/multi_buffer.rb', line 17
def add_excerpt(buffer, range, title: nil)
raise Error, "excerpt source must be a regular buffer" unless buffer.is_a?(Buffer) && !buffer.is_a?(MultiBuffer)
ending = range.end + (range.exclude_end? ? 0 : 1)
buffer.rope.byteslice(range.begin, ending - range.begin)
overlap = !@building && @excerpts.any? do |excerpt|
excerpt.buffer.equal?(buffer) && range.begin <= buffer.resolve(excerpt.last) && ending >= buffer.resolve(excerpt.first)
end
raise Error, "excerpts of the same buffer must be disjoint and nonadjacent" if overlap
label = title || "#{buffer.path || 'Untitled'}:#{buffer.rope.point_at(range.begin).row + 1}"
@excerpts << Excerpt.new(buffer, buffer.anchor(range.begin, bias: :left), buffer.anchor(ending, bias: :right), label.to_s.freeze)
@source_subscriptions[buffer] ||= buffer.on_edit { refresh unless @syncing }
refresh unless @building
self
end
|
#attach_sources(replacements) ⇒ self
Attach a privately prepared projection to live sources on the foreground
thread. Source ropes must be the exact immutable snapshots used to build
it; caller additionally validates workspace identity/version before this.
No text rebuild, regex scan or disk IO occurs here.
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
# File 'lib/canopus/multi_buffer.rb', line 36
def attach_sources(replacements)
raise Error, "cannot attach an edited projection" unless @multi_undo.empty? && @multi_redo.empty?
return self if replacements.empty?
replacements.each do |source, target|
raise Error, "unknown excerpt source" unless @source_subscriptions.key?(source)
raise Error, "excerpt source must be a regular buffer" unless target.is_a?(Buffer) && !target.is_a?(MultiBuffer)
raise Error, "source buffer is read-only" if target.read_only
raise Error, "excerpt source snapshot changed" unless source.rope.equal?(target.rope)
end
targets = @source_subscriptions.keys.map { |source| replacements.fetch(source, source) }
raise Error, "excerpt source aliases would overlap" unless targets.uniq.length == targets.length
anchors = []
subscriptions = {}
replacements.each do |source, target|
subscriptions[target] = target.on_edit { refresh unless @syncing } unless source.equal?(target)
end
@excerpts.each do |excerpt|
target = replacements[excerpt.buffer]
next unless target && !target.equal?(excerpt.buffer)
entry = [excerpt, target, target.copy_anchor(excerpt.buffer, excerpt.first)]
anchors << entry
entry << target.copy_anchor(excerpt.buffer, excerpt.last)
end
committed = true
anchors.each do |excerpt, target, first, last|
excerpt.buffer.release_anchor(excerpt.first)
excerpt.buffer.release_anchor(excerpt.last)
excerpt.buffer, excerpt.first, excerpt.last = target, first, last
end
@source_subscriptions = @source_subscriptions.to_h do |source, subscription|
target = replacements.fetch(source, source)
if source.equal?(target)
[source, subscription]
else
subscription.detach
[target, subscriptions.fetch(target)]
end
end
self
rescue StandardError
unless committed
subscriptions&.each_value(&:detach)
anchors&.each do |_, target, first, last|
target.release_anchor(first)
target.release_anchor(last) if last
end
end
raise
end
|
#begin_undo_group ⇒ self
86
87
88
89
90
|
# File 'lib/canopus/multi_buffer.rb', line 86
def begin_undo_group
@multi_undo_start = @multi_undo.length unless @undo_group_depth&.positive?
@excerpts.map(&:buffer).uniq.each(&:begin_undo_group)
super
end
|
#close ⇒ void
This method returns an undefined value.
181
182
183
184
185
186
187
|
# File 'lib/canopus/multi_buffer.rb', line 181
def close
@source_subscriptions.each_value(&:detach)
@excerpts.each do |excerpt|
excerpt.buffer.release_anchor(excerpt.first)
excerpt.buffer.release_anchor(excerpt.last)
end
end
|
#dirty? ⇒ Boolean
85
|
# File 'lib/canopus/multi_buffer.rb', line 85
def dirty? = @excerpts.any? { |excerpt| excerpt.buffer.dirty? }
|
#edit(changes, **options) ⇒ patch?
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
# File 'lib/canopus/multi_buffer.rb', line 95
def edit(changes, **options)
return super if @refreshing
return if changes.empty?
@rope.apply_edits(changes)
grouped = changes.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(range, text), result|
ending = range.end + (range.exclude_end? ? 0 : 1)
excerpt = @excerpts.find { |item| range.begin >= item.view_start && ending <= item.view_end }
raise Error, "search-result headings cannot be edited" unless excerpt
raise Error, "source buffer is read-only" if excerpt.buffer.read_only
start = excerpt.buffer.resolve(excerpt.first)
result[excerpt.buffer] << [(start + range.begin - excerpt.view_start)...(start + ending - excerpt.view_start), text]
end
patches = grouped.to_h do |buffer, edits|
[buffer, Patch.new(buffer.rope, buffer.rope.apply_edits(edits), edits)]
end
checkpoints = grouped.keys.to_h { |buffer| [buffer, buffer.send(:edit_checkpoint)] }
before = grouped.keys.to_h { |buffer| [buffer, buffer.rope] }
@syncing = true
applied = []
begin
grouped.each do |buffer, edits|
begin
buffer.edit(edits, kind: :multi_buffer)
ensure
applied << buffer unless buffer.rope.equal?(before.fetch(buffer))
end
end
result = super(changes, **options)
rebuild_text
after = grouped.keys.to_h { |buffer| [buffer, buffer.rope] }
if @undo_group_depth&.positive? && @multi_undo.length > @multi_undo_start
previous = @multi_undo.last
previous[0] = before.merge(previous.first)
previous[1] = previous.last.merge(after)
else
@multi_undo << [before, after]
end
@multi_redo.clear
result
rescue StandardError
applied.reverse_each do |buffer|
buffer.send(:restore_edit, checkpoints.fetch(buffer), patches.fetch(buffer).inverse)
rescue StandardError
end
raise
ensure
@syncing = false
end
end
|
#end_undo_group ⇒ Array[Buffer]
91
92
93
94
|
# File 'lib/canopus/multi_buffer.rb', line 91
def end_undo_group
super
@excerpts.map(&:buffer).uniq.each(&:end_undo_group)
end
|
#redo ⇒ Boolean
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
# File 'lib/canopus/multi_buffer.rb', line 162
def redo
raise Error, "end the undo group before redo" if @undo_group_depth&.positive?
group = @multi_redo.last
return false unless group
raise Error, "source changed since this excerpt edit" unless group.first.all? { |buffer, snapshot| buffer.rope.equal?(snapshot) }
@syncing = true
group.first.keys.each(&:redo)
super
@multi_undo << @multi_redo.pop
rebuild_text
true
ensure
@syncing = false
end
|
#save(path = nil, force: false) ⇒ self
176
177
178
179
180
|
# File 'lib/canopus/multi_buffer.rb', line 176
def save(path = nil, force: false)
raise Error, "Save excerpt files individually to choose new paths" if path
@excerpts.map(&:buffer).uniq.each { |buffer| buffer.save(force: force) if buffer.dirty? }
self
end
|
#undo ⇒ Boolean
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# File 'lib/canopus/multi_buffer.rb', line 148
def undo
raise Error, "end the undo group before undo" if @undo_group_depth&.positive?
group = @multi_undo.last
return false unless group
raise Error, "source changed since this excerpt edit" unless group.last.all? { |buffer, snapshot| buffer.rope.equal?(snapshot) }
@syncing = true
group.last.keys.reverse_each(&:undo)
super
@multi_redo << @multi_undo.pop
rebuild_text
true
ensure
@syncing = false
end
|