denebola 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 605dec1949038f3bf81ca5b49d875231c515fdc656933467f2d34d1b1411da7e
4
- data.tar.gz: 856f7ba61ad06df9f280850a4912259d1a76556cf32e0e5e8b8c78e6cec264e5
3
+ metadata.gz: 28307dc01292f1215dcfb5466ba9fe182633346c622d69d336456983c00276e2
4
+ data.tar.gz: 24520e65a734fb920588210d4d9b829ef0ec0997178e0ab88b708d49af604ad1
5
5
  SHA512:
6
- metadata.gz: 02af176392f474c79428f0837f7ea44ed7198a25a56a250d6119b80ddc9a9bf6f5e5eba7e020417c3c3b922ec65912b075050e6224993b73e78f4da1c8cf1d79
7
- data.tar.gz: 2d69fa29d55fb1970b30da016df0cc4730985ec286aaaa2048c08f17472f38e3904d37cefaa75961b6f3e476e1e86e3bd45fa1223dbe7101a2802dd480a2e8ed
6
+ metadata.gz: 8a29c867cdb4fa7f3e7d74c27f7556d8ecb50c08706f814d22c9ff43c90dfa776c4a4a502969a3708c67bdb9bccb8f6149c09d3656105ea97b3e925aaa378519
7
+ data.tar.gz: 36f141d074afb7bce936876e301df6d1884881c9806803619fea3d9f20f86aa0627c2ef5baaeb6c7bb5ea23041748d72c9d2891084bc9c92fce65f0ecd094edf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 — 2026-09-16
4
+
5
+ - Keep same-offset batch edit ordering deterministic across ropes and anchors.
6
+ - Transform right-biased anchors after every insertion and replacement at their boundary.
7
+
3
8
  ## 0.2.0 — 2026-09-15
4
9
 
5
10
  - Add `LazyRope` for bounded-memory access to very large UTF-8 files.
data/README.md CHANGED
@@ -109,7 +109,7 @@ rope.apply_edits([[0...5, "Hi"], [6...11, "Ruby"]]).to_s
109
109
 
110
110
  Ranges passed to `replace`, `apply_edits`, and `byteslice` address bytes in UTF-8. Bounds and codepoint boundaries are checked. Inclusive, exclusive, beginless, and endless Ruby ranges are accepted; invalid encodings, split codepoints, and overlapping batch edits raise exceptions.
111
111
 
112
- `apply_edits` applies nonoverlapping edits against one original snapshot and sorts them into source order. Adjacent ranges are allowed. Input strings are copied or shared safely with Ruby's copy-on-write strings, so later mutation of the source cannot alter a rope.
112
+ `apply_edits` applies nonoverlapping edits against one original snapshot, and `Anchor#transform` uses the same source order. Adjacent ranges are allowed; zero-length insertions at the same offset keep input order and precede a replacement beginning there. Input strings are copied or shared safely with Ruby's copy-on-write strings, so later mutation of the source cannot alter a rope.
113
113
 
114
114
  Chunks preserve extended grapheme clusters when building or joining text. A single unusually long grapheme may exceed the configured chunk size; explicit byte slices and edits may operate between codepoints inside a grapheme.
115
115
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "edit"
4
+
3
5
  module Denebola
4
6
  # A position transformed explicitly by edits against its current snapshot.
5
7
  class Anchor
@@ -15,15 +17,22 @@ module Denebola
15
17
  delta = 0
16
18
  previous = 0
17
19
  transformed = nil
18
- edits.sort_by { |range, _| range.begin }.each do |range, text|
20
+ normalized = edits.map do |range, text|
21
+ raise TypeError, "expected a Range of byte offsets" unless range.is_a?(Range)
19
22
  start = range.begin
20
23
  finish = range.end + (range.exclude_end? ? 0 : 1)
21
- raise ArgumentError, "invalid or overlapping edits" unless start.is_a?(Integer) && start >= previous && finish >= start
24
+ raise ArgumentError, "invalid edit" unless start.is_a?(Integer) && finish.is_a?(Integer) && start >= 0 && finish >= start
25
+ [start, finish, Edit.normalize_text(text)]
26
+ end
27
+ Edit.sort(normalized).each do |start, finish, text|
28
+ raise ArgumentError, "invalid or overlapping edits" if start < previous
22
29
  previous = finish
23
30
  if offset < start
24
31
  transformed ||= offset + delta
25
32
  elsif offset <= finish
26
- transformed ||= start + delta + (bias == :right ? text.bytesize : 0)
33
+ position = start + delta
34
+ transformed = position + text.bytesize if bias == :right
35
+ transformed ||= position
27
36
  end
28
37
  delta += text.bytesize - (finish - start)
29
38
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Denebola
4
+ module Edit
5
+ def self.sort(edits)
6
+ edits.each_with_index.sort_by { |edit, index| [edit[0], edit[1], index] }.map!(&:first)
7
+ end
8
+
9
+ def self.normalize_text(text)
10
+ raise TypeError, "text must be a String" unless text.is_a?(String)
11
+ string = text.encoding == Encoding::UTF_8 ? text : text.encode(Encoding::UTF_8)
12
+ raise ArgumentError, "text must be valid UTF-8" unless string.valid_encoding?
13
+ string
14
+ end
15
+ end
16
+ private_constant :Edit
17
+ end
data/lib/denebola/rope.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "edit"
4
+
3
5
  module Denebola
4
6
  class Rope
5
7
  DEFAULT_CHUNK_SIZE = 1024
@@ -22,7 +24,7 @@ module Denebola
22
24
  def initialize(text = "", chunk_size: DEFAULT_CHUNK_SIZE, branching: Tree::DEFAULT_BRANCHING, tree: nil)
23
25
  raise ArgumentError, "chunk_size must be an integer >= 4" unless chunk_size.is_a?(Integer) && chunk_size >= 4
24
26
  @chunk_size = chunk_size
25
- @tree = tree || Tree.new(chunks(normalize_text(text)), summary: TextSummary, branching: branching)
27
+ @tree = tree || Tree.new(chunks(Edit.normalize_text(text)), summary: TextSummary, branching: branching)
26
28
  freeze
27
29
  end
28
30
 
@@ -51,7 +53,7 @@ module Denebola
51
53
 
52
54
  def replace(range, text)
53
55
  start, finish = byte_bounds(range)
54
- replacement = normalize_text(text)
56
+ replacement = Edit.normalize_text(text)
55
57
  first_index, first, prefix = locate_byte_offset(start)
56
58
  last_index, last, suffix = locate_byte_offset(finish)
57
59
  if first_index.positive? && start == prefix.bytesize
@@ -76,7 +78,7 @@ module Denebola
76
78
 
77
79
  # Ranges refer to the original snapshot. Adjacent edits are allowed; overlaps are rejected.
78
80
  def apply_edits(edits)
79
- normalized = edits.map { |range, text| [*byte_bounds(range), normalize_text(text)] }.sort_by { |start, finish, _| [start, finish] }
81
+ normalized = Edit.sort(edits.map { |range, text| [*byte_bounds(range), Edit.normalize_text(text)] })
80
82
  previous = 0
81
83
  normalized.each do |start, finish, _|
82
84
  raise ArgumentError, "overlapping edits" if start < previous
@@ -234,13 +236,6 @@ module Denebola
234
236
  left.slice(0, left.size - 1).append(boundary).append(right.slice(1, right.size - 1))
235
237
  end
236
238
 
237
- def normalize_text(text)
238
- raise TypeError, "text must be a String" unless text.is_a?(String)
239
- string = text.encoding == Encoding::UTF_8 ? text : text.encode(Encoding::UTF_8)
240
- raise ArgumentError, "text must be valid UTF-8" unless string.valid_encoding?
241
- string
242
- end
243
-
244
239
  def chunks(text)
245
240
  return [] if text.empty?
246
241
  return [Chunk.new(text)] if text.bytesize <= chunk_size
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Denebola
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/denebola.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "denebola/cursor"
7
7
  require_relative "denebola/text_summary"
8
8
  require_relative "denebola/point"
9
9
  require_relative "denebola/dimensions"
10
+ require_relative "denebola/edit"
10
11
  require_relative "denebola/rope"
11
12
  require_relative "denebola/anchor"
12
13
  require_relative "denebola/lazy_rope"
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.0
4
+ version: 0.2.1
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-15 00:00:00.000000000 Z
11
+ date: 2026-09-16 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.
@@ -25,6 +25,7 @@ files:
25
25
  - lib/denebola/anchor.rb
26
26
  - lib/denebola/cursor.rb
27
27
  - lib/denebola/dimensions.rb
28
+ - lib/denebola/edit.rb
28
29
  - lib/denebola/error.rb
29
30
  - lib/denebola/lazy_rope.rb
30
31
  - lib/denebola/point.rb