denebola 0.2.1 → 0.2.2
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 +6 -0
- data/README.md +1 -1
- data/lib/denebola/lazy_rope.rb +77 -4
- data/lib/denebola/version.rb +1 -1
- data/sig/denebola.rbs +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f055c7610d7665e51b0f089b0d408584ed42eef42cc4511c9a18d46887618366
|
|
4
|
+
data.tar.gz: ced4c1c876d1e11e55089c61ce07a13679d0c53ec61a1734a8d85cb77447fd48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83906a37466f697707ec32ef73b4ef3baf60a33ac95b757a18b463139593a72b6d16fa68d033f6f5caba82720f170f192344f25f78836ac298d4b750772a277e
|
|
7
|
+
data.tar.gz: 923e8951c8cd9621b2c194be56a40b8d5fa548ecf7bd5cef17c0b4f2449b7e4ffeeee2cda26baef2f313ad0af79a8e37905bb97b668b5a550f426bb5e4a467c8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.2 — 2026-09-18
|
|
6
|
+
|
|
7
|
+
- Add persistent batched edits and bounded line-window access to `LazyRope`; related snapshots share one backing-file close lifecycle.
|
|
8
|
+
|
|
3
9
|
## 0.2.1 — 2026-09-16
|
|
4
10
|
|
|
5
11
|
- Keep same-offset batch edit ordering deterministic across ropes and anchors.
|
data/README.md
CHANGED
|
@@ -86,7 +86,7 @@ text.materialize(0...1024) # an ordinary editable Rope
|
|
|
86
86
|
text.close
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
`LazyRope` reads 1 MiB chunks by default and keeps eight chunks in an LRU cache. `byteslice`, line access, byte/codepoint positions, UTF-16 positions, and anchors follow `Rope` semantics. `edit`, `insert`, `delete`, and `replace` mutate the open view
|
|
89
|
+
`LazyRope` reads 1 MiB chunks by default and keeps eight chunks in an LRU cache. `byteslice`, line access, byte/codepoint positions, UTF-16 positions, and anchors follow `Rope` semantics. `edit`, `insert`, `delete`, and `replace` mutate the open view; `apply_edits` returns a persistent view while storing only replacement text in memory. Views in that snapshot family share one backing-file lifetime, so closing any view closes the family. Call `materialize` for an independent `Rope` snapshot. A file changed, removed, or replaced after opening raises `Denebola::Error`. `to_s` and `materialize` without a range intentionally load the complete logical file.
|
|
90
90
|
|
|
91
91
|
## Text Rope
|
|
92
92
|
|
data/lib/denebola/lazy_rope.rb
CHANGED
|
@@ -10,6 +10,20 @@ module Denebola
|
|
|
10
10
|
TextPiece = Struct.new(:rope, keyword_init: true) do
|
|
11
11
|
def bytesize = rope.bytesize
|
|
12
12
|
end
|
|
13
|
+
class FileOwner
|
|
14
|
+
def initialize(file)
|
|
15
|
+
@file = file
|
|
16
|
+
@lock = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def closed? = @file.closed?
|
|
20
|
+
|
|
21
|
+
def close
|
|
22
|
+
@lock.synchronize { @file.close unless @file.closed? }
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
private_constant :FileOwner
|
|
13
27
|
|
|
14
28
|
attr_reader :chunk_size, :cached_bytes
|
|
15
29
|
|
|
@@ -25,6 +39,7 @@ module Denebola
|
|
|
25
39
|
flags = File::RDONLY | File::BINARY
|
|
26
40
|
flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE)
|
|
27
41
|
@file = File.open(path, flags)
|
|
42
|
+
@file_owner = FileOwner.new(@file)
|
|
28
43
|
@path = File.expand_path(path)
|
|
29
44
|
@chunk_size = chunk_size
|
|
30
45
|
@cache_chunks = cache_chunks
|
|
@@ -45,7 +60,7 @@ module Denebola
|
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
def lazy? = true
|
|
48
|
-
def closed? = @
|
|
63
|
+
def closed? = @file_owner.closed?
|
|
49
64
|
|
|
50
65
|
def bytesize
|
|
51
66
|
ensure_unchanged!
|
|
@@ -94,6 +109,26 @@ module Denebola
|
|
|
94
109
|
read_bytes(start, ending - start).force_encoding(Encoding::UTF_8).sub(/(?:\r\n|[\r\n\u2028\u2029])\z/, "")
|
|
95
110
|
end
|
|
96
111
|
|
|
112
|
+
def line_end(row)
|
|
113
|
+
start = line_start(row)
|
|
114
|
+
index_until_line(row + 1)
|
|
115
|
+
ending = row + 1 < packed_line_count ? unpack_line_start(row + 1) : bytesize
|
|
116
|
+
tail = read_bytes([ending - 3, start].max, [ending - start, 3].min)
|
|
117
|
+
ending - (tail.end_with?("\r\n") ? 2 : tail.end_with?("\xE2\x80\xA8".b, "\xE2\x80\xA9".b) ? 3 : tail.end_with?("\r", "\n") ? 1 : 0)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def line_window(row, from: 0, max_bytes: 16_384)
|
|
121
|
+
raise TypeError, "from must be an Integer" unless from.is_a?(Integer)
|
|
122
|
+
raise ArgumentError, "max_bytes must be a nonnegative integer" unless max_bytes.is_a?(Integer) && max_bytes >= 0
|
|
123
|
+
|
|
124
|
+
start, ending = line_start(row), line_end(row)
|
|
125
|
+
offset = (start + from).clamp(start, ending)
|
|
126
|
+
offset -= 1 while offset > start && offset < ending && (read_bytes(offset, 1).getbyte(0) & 0xC0) == 0x80
|
|
127
|
+
value = read_bytes(offset, [max_bytes, ending - offset].min)
|
|
128
|
+
value = value.byteslice(0, complete_utf8_length(value)) unless value.empty?
|
|
129
|
+
[value.force_encoding(Encoding::UTF_8), offset - start]
|
|
130
|
+
end
|
|
131
|
+
|
|
97
132
|
def byteslice(offset, length = nil)
|
|
98
133
|
start, finish = slice_bounds(offset, length)
|
|
99
134
|
Rope.new(read_bytes(start, finish - start).force_encoding(Encoding::UTF_8))
|
|
@@ -149,6 +184,34 @@ module Denebola
|
|
|
149
184
|
def delete(range) = edit(range, "")
|
|
150
185
|
def replace(range, text) = edit(range, text)
|
|
151
186
|
|
|
187
|
+
# Ranges address the original snapshot. The returned view has independent
|
|
188
|
+
# overlays, indexes, and cache. Snapshots share one backing-file lifetime:
|
|
189
|
+
# closing any snapshot closes the complete family.
|
|
190
|
+
def apply_edits(edits)
|
|
191
|
+
normalized = Edit.sort(edits.map do |range, text|
|
|
192
|
+
start, finish = byte_bounds(range)
|
|
193
|
+
validate_offset(start)
|
|
194
|
+
validate_offset(finish)
|
|
195
|
+
[start, finish, normalize_text(text)]
|
|
196
|
+
end)
|
|
197
|
+
previous = 0
|
|
198
|
+
normalized.each do |start, finish, _text|
|
|
199
|
+
raise ArgumentError, "overlapping edits" if start < previous
|
|
200
|
+
previous = finish
|
|
201
|
+
end
|
|
202
|
+
return self if normalized.empty?
|
|
203
|
+
|
|
204
|
+
updated = []
|
|
205
|
+
consumed = 0
|
|
206
|
+
normalized.each do |start, finish, text|
|
|
207
|
+
updated.concat(pieces_for(consumed, start))
|
|
208
|
+
updated << TextPiece.new(rope: Rope.new(text)).freeze unless text.empty?
|
|
209
|
+
consumed = finish
|
|
210
|
+
end
|
|
211
|
+
updated.concat(pieces_for(consumed, bytesize))
|
|
212
|
+
snapshot_with(coalesce(updated))
|
|
213
|
+
end
|
|
214
|
+
|
|
152
215
|
def point_at(byte_offset)
|
|
153
216
|
validate_offset(byte_offset)
|
|
154
217
|
index_to([byte_offset + 1, bytesize].min)
|
|
@@ -236,8 +299,7 @@ module Denebola
|
|
|
236
299
|
end
|
|
237
300
|
|
|
238
301
|
def close
|
|
239
|
-
@
|
|
240
|
-
nil
|
|
302
|
+
@file_owner.close
|
|
241
303
|
end
|
|
242
304
|
|
|
243
305
|
private
|
|
@@ -314,6 +376,17 @@ module Denebola
|
|
|
314
376
|
@piece_ends = @pieces.map { |piece| total += piece.bytesize }
|
|
315
377
|
end
|
|
316
378
|
|
|
379
|
+
def snapshot_with(pieces)
|
|
380
|
+
ensure_unchanged!
|
|
381
|
+
snapshot = dup
|
|
382
|
+
snapshot.instance_variable_set(:@cache, {})
|
|
383
|
+
snapshot.instance_variable_set(:@cached_bytes, 0)
|
|
384
|
+
snapshot.instance_variable_set(:@pieces, pieces.freeze)
|
|
385
|
+
snapshot.__send__(:rebuild_piece_ends)
|
|
386
|
+
snapshot.__send__(:reset_index)
|
|
387
|
+
snapshot
|
|
388
|
+
end
|
|
389
|
+
|
|
317
390
|
def read_bytes(offset, count)
|
|
318
391
|
ensure_unchanged!
|
|
319
392
|
return "".b if count.zero?
|
|
@@ -562,7 +635,7 @@ module Denebola
|
|
|
562
635
|
end
|
|
563
636
|
|
|
564
637
|
def ensure_unchanged!
|
|
565
|
-
raise IOError, "closed file" if
|
|
638
|
+
raise IOError, "closed file" if closed?
|
|
566
639
|
unchanged = file_stamp(@file.stat) == @stamp && file_stamp(File.stat(@path)) == @path_stamp
|
|
567
640
|
raise Error, "file changed on disk; reopen it" unless unchanged
|
|
568
641
|
rescue Errno::ENOENT
|
data/lib/denebola/version.rb
CHANGED
data/sig/denebola.rbs
CHANGED
|
@@ -162,7 +162,9 @@ module Denebola
|
|
|
162
162
|
def utf16_length: () -> Integer
|
|
163
163
|
def line_count: (?exact: bool) -> Integer
|
|
164
164
|
def line_start: (Integer) -> Integer
|
|
165
|
+
def line_end: (Integer) -> Integer
|
|
165
166
|
def line: (Integer) -> String
|
|
167
|
+
def line_window: (Integer, ?from: Integer, ?max_bytes: Integer) -> [String, Integer]
|
|
166
168
|
def byteslice: (Integer | Range[Integer?], ?Integer?) -> Rope
|
|
167
169
|
def each_chunk: () { (String) -> void } -> self
|
|
168
170
|
| () -> Enumerator[String, self]
|
|
@@ -172,6 +174,7 @@ module Denebola
|
|
|
172
174
|
def insert: (Integer, String) -> self
|
|
173
175
|
def delete: (Range[Integer?]) -> self
|
|
174
176
|
def replace: (Range[Integer?], String) -> self
|
|
177
|
+
def apply_edits: (Array[[Range[Integer?], String]]) -> LazyRope
|
|
175
178
|
def point_at: (Integer) -> Point
|
|
176
179
|
def offset_at: (Point) -> Integer
|
|
177
180
|
def utf16_offset_at: (Integer) -> Integer
|
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.
|
|
4
|
+
version: 0.2.2
|
|
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-18 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.
|