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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Denebola
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/denebola.rb CHANGED
@@ -11,3 +11,5 @@ require_relative "denebola/edit"
11
11
  require_relative "denebola/rope"
12
12
  require_relative "denebola/anchor"
13
13
  require_relative "denebola/lazy_rope"
14
+ require_relative "denebola/sheet"
15
+ require_relative "denebola/sheet_range_index"
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.2.2
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-18 00:00:00.000000000 Z
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