denebola 0.2.2 → 0.4.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 +8 -1
- data/README.md +57 -1
- data/lib/denebola/code_editor_buffer.rb +52 -0
- data/lib/denebola/sheet.rb +796 -0
- data/lib/denebola/sheet_range_index.rb +327 -0
- data/lib/denebola/version.rb +1 -1
- data/lib/denebola.rb +2 -0
- data/sig/denebola.rbs +45 -0
- metadata +5 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Denebola
|
|
4
|
+
class Sheet
|
|
5
|
+
# A persistent row segment tree whose nodes cache a persistent column segment
|
|
6
|
+
# tree. Point edits touch log(row extent) * log(column extent) index nodes.
|
|
7
|
+
class RangeIndex
|
|
8
|
+
class ValueNode
|
|
9
|
+
attr_reader :key, :count, :left, :right, :height, :minimum, :maximum
|
|
10
|
+
|
|
11
|
+
def initialize(key, count, left, right)
|
|
12
|
+
@key, @count, @left, @right = key, count, left, right
|
|
13
|
+
@height = 1 + [left&.height || 0, right&.height || 0].max
|
|
14
|
+
@minimum = left ? left.minimum : key
|
|
15
|
+
@maximum = right ? right.maximum : key
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ValueTree
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def update(root, key, delta)
|
|
24
|
+
if root.nil?
|
|
25
|
+
raise "missing value in range index" unless delta.positive?
|
|
26
|
+
return ValueNode.new(key, delta, nil, nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
comparison = key <=> root.key
|
|
30
|
+
raise ArgumentError, "numeric cell values are not mutually comparable" unless comparison
|
|
31
|
+
|
|
32
|
+
if comparison.zero?
|
|
33
|
+
count = root.count + delta
|
|
34
|
+
return remove_root(root) if count.zero?
|
|
35
|
+
raise "negative value count in range index" if count.negative?
|
|
36
|
+
return ValueNode.new(root.key, count, root.left, root.right)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if comparison.negative?
|
|
40
|
+
balance(ValueNode.new(root.key, root.count, update(root.left, key, delta), root.right))
|
|
41
|
+
else
|
|
42
|
+
balance(ValueNode.new(root.key, root.count, root.left, update(root.right, key, delta)))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove_root(root)
|
|
47
|
+
return root.right unless root.left
|
|
48
|
+
return root.left unless root.right
|
|
49
|
+
|
|
50
|
+
successor = root.right
|
|
51
|
+
successor = successor.left while successor.left
|
|
52
|
+
right = update(root.right, successor.key, -successor.count)
|
|
53
|
+
balance(ValueNode.new(successor.key, successor.count, root.left, right))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def balance(root)
|
|
57
|
+
factor = height(root.left) - height(root.right)
|
|
58
|
+
if factor > 1
|
|
59
|
+
left = root.left
|
|
60
|
+
left = rotate_left(left) if height(left.left) < height(left.right)
|
|
61
|
+
return rotate_right(ValueNode.new(root.key, root.count, left, root.right))
|
|
62
|
+
elsif factor < -1
|
|
63
|
+
right = root.right
|
|
64
|
+
right = rotate_right(right) if height(right.right) < height(right.left)
|
|
65
|
+
return rotate_left(ValueNode.new(root.key, root.count, root.left, right))
|
|
66
|
+
end
|
|
67
|
+
root
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rotate_left(root)
|
|
71
|
+
pivot = root.right
|
|
72
|
+
left = ValueNode.new(root.key, root.count, root.left, pivot.left)
|
|
73
|
+
ValueNode.new(pivot.key, pivot.count, left, pivot.right)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def rotate_right(root)
|
|
77
|
+
pivot = root.left
|
|
78
|
+
right = ValueNode.new(root.key, root.count, pivot.right, root.right)
|
|
79
|
+
ValueNode.new(pivot.key, pivot.count, pivot.left, right)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def height(node) = node&.height || 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class ValueBag
|
|
86
|
+
attr_reader :summary, :count
|
|
87
|
+
|
|
88
|
+
def initialize(count: 0, sum: 0, types: {}, numbers: nil)
|
|
89
|
+
@count, @sum, @types, @numbers = count, sum, types.freeze, numbers
|
|
90
|
+
@summary = Sheet::Summary.new(count: count, sum: sum,
|
|
91
|
+
min: numbers&.minimum, max: numbers&.maximum, types: @types)
|
|
92
|
+
freeze
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ZERO = new
|
|
96
|
+
|
|
97
|
+
def self.zero = ZERO
|
|
98
|
+
|
|
99
|
+
def replace(old_value, new_value)
|
|
100
|
+
bag = old_value.nil? ? self : update(old_value, -1)
|
|
101
|
+
new_value.nil? ? bag : bag.update(new_value, 1)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def update(value, delta)
|
|
105
|
+
return self if delta.zero?
|
|
106
|
+
|
|
107
|
+
types = @types.dup
|
|
108
|
+
type_count = types.fetch(value.class, 0) + delta
|
|
109
|
+
raise "negative type count in range index" if type_count.negative?
|
|
110
|
+
type_count.zero? ? types.delete(value.class) : types[value.class] = type_count
|
|
111
|
+
sum = @sum + (value.is_a?(Numeric) ? value * delta : 0)
|
|
112
|
+
numbers = if comparable_number?(value)
|
|
113
|
+
ValueTree.update(@numbers, value, delta)
|
|
114
|
+
else
|
|
115
|
+
@numbers
|
|
116
|
+
end
|
|
117
|
+
self.class.new(count: @count + delta, sum: sum, types: types, numbers: numbers)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def empty? = count.zero?
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def comparable_number?(value)
|
|
125
|
+
value.is_a?(Numeric) && !(value <=> value).nil?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
class ColumnNode
|
|
130
|
+
attr_reader :left, :right, :bag, :summary
|
|
131
|
+
|
|
132
|
+
def initialize(left: nil, right: nil, bag: nil)
|
|
133
|
+
@left, @right, @bag = left, right, bag
|
|
134
|
+
@summary = bag ? bag.summary : summary_of(left) + summary_of(right)
|
|
135
|
+
freeze
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def summary_of(node) = node&.summary || Sheet::Summary.zero
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class ColumnIndex
|
|
144
|
+
attr_reader :root, :capacity
|
|
145
|
+
|
|
146
|
+
def initialize(root: nil, capacity: 1)
|
|
147
|
+
@root, @capacity = root, capacity
|
|
148
|
+
freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def update(column, old_value, new_value)
|
|
152
|
+
return self if old_value == new_value
|
|
153
|
+
|
|
154
|
+
root, capacity = @root, @capacity
|
|
155
|
+
while column >= capacity
|
|
156
|
+
root = ColumnNode.new(left: root, right: nil) if root
|
|
157
|
+
capacity *= 2
|
|
158
|
+
end
|
|
159
|
+
root = update_node(root, 0, capacity, column, old_value, new_value)
|
|
160
|
+
self.class.new(root: root, capacity: capacity)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def summary(left, right)
|
|
164
|
+
return Sheet::Summary.zero unless @root && left < @capacity
|
|
165
|
+
|
|
166
|
+
query(@root, 0, @capacity, left, [right + 1, @capacity].min)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def empty? = @root.nil?
|
|
170
|
+
|
|
171
|
+
def check_invariants!
|
|
172
|
+
check_node(@root, 0, @capacity) if @root
|
|
173
|
+
true
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def update_node(node, first, finish, column, old_value, new_value)
|
|
179
|
+
if finish - first == 1
|
|
180
|
+
bag = (node&.bag || ValueBag.zero).replace(old_value, new_value)
|
|
181
|
+
return nil if bag.empty?
|
|
182
|
+
|
|
183
|
+
return ColumnNode.new(bag: bag)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
middle = (first + finish) / 2
|
|
187
|
+
left, right = node&.left, node&.right
|
|
188
|
+
if column < middle
|
|
189
|
+
left = update_node(left, first, middle, column, old_value, new_value)
|
|
190
|
+
else
|
|
191
|
+
right = update_node(right, middle, finish, column, old_value, new_value)
|
|
192
|
+
end
|
|
193
|
+
return nil unless left || right
|
|
194
|
+
|
|
195
|
+
ColumnNode.new(left: left, right: right)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def query(node, first, finish, left, right)
|
|
199
|
+
return Sheet::Summary.zero unless node && left < finish && first < right
|
|
200
|
+
return node.summary if left <= first && finish <= right
|
|
201
|
+
|
|
202
|
+
middle = (first + finish) / 2
|
|
203
|
+
query(node.left, first, middle, left, right) + query(node.right, middle, finish, left, right)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def check_node(node, first, finish)
|
|
207
|
+
if finish - first == 1
|
|
208
|
+
raise "invalid column-index leaf" unless node.bag && !node.left && !node.right && node.summary == node.bag.summary
|
|
209
|
+
return node.summary
|
|
210
|
+
end
|
|
211
|
+
raise "invalid column-index branch" if node.bag
|
|
212
|
+
middle = (first + finish) / 2
|
|
213
|
+
summary = (node.left ? check_node(node.left, first, middle) : Sheet::Summary.zero) +
|
|
214
|
+
(node.right ? check_node(node.right, middle, finish) : Sheet::Summary.zero)
|
|
215
|
+
raise "incorrect column-index summary" unless node.summary == summary
|
|
216
|
+
summary
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class RowNode
|
|
221
|
+
attr_reader :left, :right, :columns
|
|
222
|
+
|
|
223
|
+
def initialize(left, right, columns)
|
|
224
|
+
@left, @right, @columns = left, right, columns
|
|
225
|
+
freeze
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
attr_reader :root, :capacity
|
|
230
|
+
|
|
231
|
+
def initialize(root: nil, capacity: 1)
|
|
232
|
+
@root, @capacity = root, capacity
|
|
233
|
+
freeze
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def update(row, column, old_value, new_value)
|
|
237
|
+
return self if unchanged_value?(old_value, new_value)
|
|
238
|
+
|
|
239
|
+
root, capacity = @root, @capacity
|
|
240
|
+
while row >= capacity
|
|
241
|
+
root = RowNode.new(root, nil, root.columns) if root
|
|
242
|
+
capacity *= 2
|
|
243
|
+
end
|
|
244
|
+
root = update_row(root, 0, capacity, row, column, old_value, new_value)
|
|
245
|
+
self.class.new(root: root, capacity: capacity)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def summary(top, left, bottom, right)
|
|
249
|
+
return Sheet::Summary.zero unless @root && top < @capacity
|
|
250
|
+
|
|
251
|
+
query(@root, 0, @capacity, top, [bottom + 1, @capacity].min, left, right)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.from_rows(rows, row_axis, column_axis)
|
|
255
|
+
index = new
|
|
256
|
+
row = 0
|
|
257
|
+
rows.each do |item|
|
|
258
|
+
if item.is_a?(Gap)
|
|
259
|
+
row += item.length
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
column = 0
|
|
263
|
+
item.columns.each do |cell|
|
|
264
|
+
if cell.is_a?(Gap)
|
|
265
|
+
column += cell.length
|
|
266
|
+
else
|
|
267
|
+
index = index.update(row_axis.index_at(row), column_axis.index_at(column), nil, cell.value)
|
|
268
|
+
column += 1
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
row += 1
|
|
272
|
+
end
|
|
273
|
+
index
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def check_invariants!
|
|
277
|
+
check_row(@root, 0, @capacity) if @root
|
|
278
|
+
true
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def unchanged_value?(old_value, new_value)
|
|
284
|
+
old_value.nil? ? new_value.nil? : !new_value.nil? && old_value.class == new_value.class && old_value == new_value
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def update_row(node, first, finish, row, column, old_value, new_value)
|
|
288
|
+
columns = (node&.columns || ColumnIndex.new).update(column, old_value, new_value)
|
|
289
|
+
if finish - first == 1
|
|
290
|
+
return nil if columns.empty?
|
|
291
|
+
return RowNode.new(nil, nil, columns)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
middle = (first + finish) / 2
|
|
295
|
+
left, right = node&.left, node&.right
|
|
296
|
+
if row < middle
|
|
297
|
+
left = update_row(left, first, middle, row, column, old_value, new_value)
|
|
298
|
+
else
|
|
299
|
+
right = update_row(right, middle, finish, row, column, old_value, new_value)
|
|
300
|
+
end
|
|
301
|
+
return nil if columns.empty?
|
|
302
|
+
|
|
303
|
+
RowNode.new(left, right, columns)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def query(node, first, finish, top, bottom, left, right)
|
|
307
|
+
return Sheet::Summary.zero unless node && top < finish && first < bottom
|
|
308
|
+
return node.columns.summary(left, right) if top <= first && finish <= bottom
|
|
309
|
+
|
|
310
|
+
middle = (first + finish) / 2
|
|
311
|
+
query(node.left, first, middle, top, bottom, left, right) +
|
|
312
|
+
query(node.right, middle, finish, top, bottom, left, right)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def check_row(node, first, finish)
|
|
316
|
+
node.columns.check_invariants!
|
|
317
|
+
return if finish - first == 1
|
|
318
|
+
|
|
319
|
+
middle = (first + finish) / 2
|
|
320
|
+
check_row(node.left, first, middle) if node.left
|
|
321
|
+
check_row(node.right, middle, finish) if node.right
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private_constant :RangeIndex
|
|
326
|
+
end
|
|
327
|
+
end
|
data/lib/denebola/version.rb
CHANGED
data/lib/denebola.rb
CHANGED
data/sig/denebola.rbs
CHANGED
|
@@ -186,10 +186,55 @@ module Denebola
|
|
|
186
186
|
def close: () -> nil
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
class CodeEditorBuffer
|
|
190
|
+
attr_reader rope: Rope
|
|
191
|
+
def initialize: (?Rope) -> void
|
|
192
|
+
def line_count: () -> Integer
|
|
193
|
+
def line: (Integer) -> String
|
|
194
|
+
def line_start: (Integer) -> Integer
|
|
195
|
+
def line_of: (Integer) -> Integer
|
|
196
|
+
def to_s: () -> String
|
|
197
|
+
def replace: (Range[Integer?], String) -> self
|
|
198
|
+
def undo: () -> self
|
|
199
|
+
def redo: () -> self
|
|
200
|
+
def can_undo?: () -> bool
|
|
201
|
+
def can_redo?: () -> bool
|
|
202
|
+
end
|
|
203
|
+
|
|
189
204
|
class Anchor
|
|
190
205
|
attr_reader offset: Integer
|
|
191
206
|
attr_reader bias: :left | :right
|
|
192
207
|
def initialize: (Integer, ?bias: :left | :right) -> void
|
|
193
208
|
def transform: (Array[[Range[Integer], String]]) -> Anchor
|
|
194
209
|
end
|
|
210
|
+
|
|
211
|
+
class Sheet
|
|
212
|
+
DEFAULT_BRANCHING: Integer
|
|
213
|
+
attr_reader row_count: Integer
|
|
214
|
+
attr_reader column_count: Integer
|
|
215
|
+
attr_reader cell_count: Integer
|
|
216
|
+
def initialize: (?branching: Integer) -> void
|
|
217
|
+
def []: (Integer, Integer) -> untyped
|
|
218
|
+
def set: (Integer, Integer, untyped) -> Sheet
|
|
219
|
+
def set_many: (Enumerable[[Integer, Integer, untyped]]) -> Sheet
|
|
220
|
+
def delete: (Integer, Integer) -> Sheet
|
|
221
|
+
def each_in: (Integer, Integer, Integer, Integer) { (Point, untyped) -> void } -> self
|
|
222
|
+
| (Integer, Integer, Integer, Integer) -> Enumerator[[Point, untyped], self]
|
|
223
|
+
def summary: (Integer, Integer, Integer, Integer) -> Summary
|
|
224
|
+
def insert_rows: (Integer, Integer) -> Sheet
|
|
225
|
+
def delete_rows: (Integer, Integer) -> Sheet
|
|
226
|
+
def insert_columns: (Integer, Integer) -> Sheet
|
|
227
|
+
def delete_columns: (Integer, Integer) -> Sheet
|
|
228
|
+
def snapshot: () -> Sheet
|
|
229
|
+
def check_invariants!: () -> true
|
|
230
|
+
|
|
231
|
+
class Summary
|
|
232
|
+
attr_reader count: Integer
|
|
233
|
+
attr_reader sum: Numeric
|
|
234
|
+
attr_reader min: Numeric?
|
|
235
|
+
attr_reader max: Numeric?
|
|
236
|
+
attr_reader types: Hash[Class, Integer]
|
|
237
|
+
def self.zero: () -> Summary
|
|
238
|
+
end
|
|
239
|
+
end
|
|
195
240
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: denebola
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Immutable text ropes plus bounded-memory, file-backed editing with line
|
|
14
14
|
and UTF-16 indexing.
|
|
@@ -23,6 +23,7 @@ files:
|
|
|
23
23
|
- README.md
|
|
24
24
|
- lib/denebola.rb
|
|
25
25
|
- lib/denebola/anchor.rb
|
|
26
|
+
- lib/denebola/code_editor_buffer.rb
|
|
26
27
|
- lib/denebola/cursor.rb
|
|
27
28
|
- lib/denebola/dimensions.rb
|
|
28
29
|
- lib/denebola/edit.rb
|
|
@@ -30,6 +31,8 @@ files:
|
|
|
30
31
|
- lib/denebola/lazy_rope.rb
|
|
31
32
|
- lib/denebola/point.rb
|
|
32
33
|
- lib/denebola/rope.rb
|
|
34
|
+
- lib/denebola/sheet.rb
|
|
35
|
+
- lib/denebola/sheet_range_index.rb
|
|
33
36
|
- lib/denebola/text_summary.rb
|
|
34
37
|
- lib/denebola/tree.rb
|
|
35
38
|
- lib/denebola/version.rb
|