Class: Canopus::LazyRope

Inherits:
Object
  • Object
show all
Defined in:
lib/canopus/lazy_rope.rb,
sig/canopus.rbs

Overview

Read-only UTF-8 rope facade. Newline offsets are packed; document bytes stay on disk and a bounded LRU serves positioned pages independently of IO position.

Constant Summary collapse

PAGE_SIZE =

Returns:

  • (Integer)
65_536

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, page_limit: 32) ⇒ LazyRope

Returns a new instance of LazyRope.

Parameters:

  • path (String)
  • page_limit: (Integer) (defaults to: 32)


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/canopus/lazy_rope.rb', line 9

def initialize(path, page_limit: 32)
  raise ArgumentError, "page limit must be positive" unless page_limit.is_a?(Integer) && page_limit.positive?
  @file = File.open(path, File::RDONLY | File::BINARY | File::SHARE_DELETE)
  @positioned_read, @read_lock = true, Mutex.new
  @stamp = stamp
  @origin = @file.size >= 3 && read_at(3, 0) == "\xEF\xBB\xBF".b ? 3 : 0
  @bytesize = @file.size - @origin
  @pages, @page_limit, @cached_bytes = {}, page_limit, 0
  @starts = [0].pack("Q<")
  @checkpoints = [0, 0, 0].pack("Q<3")
  @length = @utf16_length = 0
  index_lines
  @starts.freeze
  @checkpoints.freeze
  @line_ending ||= "\n"
rescue StandardError
  @file&.close
  raise
end

Instance Attribute Details

#bytesize ⇒ Integer (readonly)

Returns the value of attribute bytesize.

Returns:

  • (Integer)


8
9
10
# File 'lib/canopus/lazy_rope.rb', line 8

def bytesize
  @bytesize
end

#cached_bytes ⇒ Integer (readonly)

Returns the value of attribute cached_bytes.

Returns:

  • (Integer)


8
9
10
# File 'lib/canopus/lazy_rope.rb', line 8

def cached_bytes
  @cached_bytes
end

#line_ending ⇒ String (readonly)

Returns the value of attribute line_ending.

Returns:

  • (String)


8
9
10
# File 'lib/canopus/lazy_rope.rb', line 8

def line_ending
  @line_ending
end

Instance Method Details

#apply_edits ⇒ bot

Parameters:

  • changes (changes)

Returns:

  • (bot)

Raises:



115
# File 'lib/canopus/lazy_rope.rb', line 115

def apply_edits(*) = raise(Error, "large file is read-only")

#byteslice(offset, count) ⇒ Denebola::Rope #byteslice(range) ⇒ Denebola::Rope

Overloads:

  • #byteslice(offset, count) ⇒ Denebola::Rope

    Parameters:

    • offset (Integer)
    • count (Integer, nil)

    Returns:

    • (Denebola::Rope)
  • #byteslice(range) ⇒ Denebola::Rope

    Parameters:

    • range (Range[Integer])

    Returns:

    • (Denebola::Rope)

Raises:

  • (RangeError)


57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/canopus/lazy_rope.rb', line 57

def byteslice(offset, count = nil)
  if offset.is_a?(Range)
    range = offset
    offset = range.begin
    count = range.end + (range.exclude_end? ? 0 : 1) - offset
  end
  count ||= @bytesize - offset
  validate_offset(offset)
  validate_offset(offset + count)
  raise RangeError, "negative slice length" if count.negative?
  Denebola::Rope.new(read_bytes(offset, count).force_encoding(Encoding::UTF_8))
end

#check_invariants! ⇒ true

Returns:

  • (true)

Raises:



116
117
118
119
# File 'lib/canopus/lazy_rope.rb', line 116

def check_invariants!
  raise Error, "invalid line index" unless line_start(0).zero? && @starts.bytesize % 8 == 0
  true
end

#close ⇒ void

This method returns an undefined value.



33
# File 'lib/canopus/lazy_rope.rb', line 33

def close = (@file.close unless @file.closed?)

#each_chunk ⇒ Enumerator[String, void] #each_chunk ⇒ void

Overloads:

  • #each_chunk ⇒ Enumerator[String, void]

    Returns:

    • (Enumerator[String, void])
  • #each_chunk ⇒ void

    This method returns an undefined value.

Yields:

Yield Parameters:

  • arg0 (String)

Yield Returns:

  • (void)


103
104
105
106
107
108
109
110
111
112
113
# File 'lib/canopus/lazy_rope.rb', line 103

def each_chunk
  return enum_for(__method__) unless block_given?
  offset = 0
  while offset < @bytesize
    value = read_bytes(offset, [PAGE_SIZE, @bytesize - offset].min)
    finish = complete_utf8_length(value)
    raise Error, "invalid UTF-8 file" if finish.zero?
    yield value.byteslice(0, finish).force_encoding(Encoding::UTF_8)
    offset += finish
  end
end

#empty? ⇒ Boolean

Returns:

  • (Boolean)


32
# File 'lib/canopus/lazy_rope.rb', line 32

def empty? = @bytesize.zero?

#lazy? ⇒ Boolean

Returns:

  • (Boolean)


28
# File 'lib/canopus/lazy_rope.rb', line 28

def lazy? = true

#length ⇒ Integer

Returns:

  • (Integer)


30
# File 'lib/canopus/lazy_rope.rb', line 30

def length = @length

#line(row) ⇒ String

Parameters:

  • row (Integer)

Returns:

  • (String)


38
39
40
41
42
# File 'lib/canopus/lazy_rope.rb', line 38

def line(row)
  start = line_start(row)
  ending = row + 1 < line_count ? line_start(row + 1) : @bytesize
  read_bytes(start, ending - start).force_encoding(Encoding::UTF_8).sub(/(?:\r\n|[\r\n\u2028\u2029])\z/, "")
end

#line_count ⇒ Integer

Returns:

  • (Integer)


29
# File 'lib/canopus/lazy_rope.rb', line 29

def line_count = @starts.bytesize / 8

#line_end(row) ⇒ Integer

Parameters:

  • row (Integer)

Returns:

  • (Integer)


43
44
45
46
47
48
# File 'lib/canopus/lazy_rope.rb', line 43

def line_end(row)
  ending = row + 1 < line_count ? line_start(row + 1) : @bytesize
  return ending if row + 1 == line_count
  tail = read_bytes([ending - 3, 0].max, [ending, 3].min)
  ending - (tail.end_with?("\r\n") ? 2 : tail.end_with?("\xE2\x80\xA8".b, "\xE2\x80\xA9".b) ? 3 : 1)
end

#line_start(row) ⇒ Integer

Parameters:

  • row (Integer)

Returns:

  • (Integer)

Raises:

  • (RangeError)


34
35
36
37
# File 'lib/canopus/lazy_rope.rb', line 34

def line_start(row)
  raise RangeError, "line outside document" unless row.is_a?(Integer) && row.between?(0, line_count - 1)
  @starts.unpack1("Q<", offset: row * 8)
end

#line_window(row, from: 0, max_bytes: 16_384) ⇒ [String, Integer]

Parameters:

  • row (Integer)
  • from: (Integer) (defaults to: 0)
  • max_bytes: (Integer) (defaults to: 16_384)

Returns:

  • ([String, Integer])


49
50
51
52
53
54
55
56
# File 'lib/canopus/lazy_rope.rb', line 49

def line_window(row, from: 0, max_bytes: 16_384)
  start, ending = line_start(row), line_end(row)
  offset = (start + from).clamp(start, ending)
  offset -= 1 while offset > start && offset < ending && (read_bytes(offset, 1).getbyte(0) & 0xc0) == 0x80
  value = read_bytes(offset, [max_bytes, ending - offset].min)
  value = value.byteslice(0, complete_utf8_length(value)) unless value.empty?
  [value.force_encoding(Encoding::UTF_8), offset - start]
end

#offset_at(point) ⇒ Integer

Parameters:

  • point (Denebola::Point)

Returns:

  • (Integer)

Raises:

  • (RangeError)


79
80
81
82
83
84
# File 'lib/canopus/lazy_rope.rb', line 79

def offset_at(point)
  start, ending = line_start(point.row), line_end(point.row)
  base = prefix_count(start)
  raise RangeError, "column outside line" unless point.column.is_a?(Integer) && point.column.between?(0, prefix_count(ending) - base)
  offset_for_count(base + point.column)
end

#offset_at_utf16(units) ⇒ Integer

Parameters:

  • units (Integer)

Returns:

  • (Integer)

Raises:

  • (RangeError)


99
100
101
102
# File 'lib/canopus/lazy_rope.rb', line 99

def offset_at_utf16(units)
  raise RangeError, "UTF-16 offset outside document" unless units.is_a?(Integer) && units.between?(0, @utf16_length)
  offset_for_count(units, utf16: true)
end

#offset_at_utf16_point(point) ⇒ Integer

Parameters:

  • point (Denebola::Point)

Returns:

  • (Integer)

Raises:

  • (RangeError)


89
90
91
92
93
94
# File 'lib/canopus/lazy_rope.rb', line 89

def offset_at_utf16_point(point)
  base = prefix_count(line_start(point.row), utf16: true)
  ending = prefix_count(line_end(point.row), utf16: true)
  raise RangeError, "UTF-16 column outside line" unless point.column.is_a?(Integer) && point.column.between?(0, ending - base)
  offset_for_count(base + point.column, utf16: true)
end

#point_at(offset) ⇒ Denebola::Point

Parameters:

  • offset (Integer)

Returns:

  • (Denebola::Point)


69
70
71
72
73
74
75
76
77
78
# File 'lib/canopus/lazy_rope.rb', line 69

def point_at(offset)
  validate_offset(offset)
  low, high = 0, line_count
  while low < high
    middle = (low + high) / 2
    line_start(middle) <= offset ? low = middle + 1 : high = middle
  end
  row = [low - 1, 0].max
  Denebola::Point.new(row, prefix_count(offset) - prefix_count(line_start(row)))
end

#to_s ⇒ String

Returns:

  • (String)


114
# File 'lib/canopus/lazy_rope.rb', line 114

def to_s = each_chunk.to_a.join

#utf16_length ⇒ Integer

Returns:

  • (Integer)


31
# File 'lib/canopus/lazy_rope.rb', line 31

def utf16_length = @utf16_length

#utf16_offset_at(offset) ⇒ Integer

Parameters:

  • offset (Integer)

Returns:

  • (Integer)


95
96
97
98
# File 'lib/canopus/lazy_rope.rb', line 95

def utf16_offset_at(offset)
  validate_offset(offset)
  prefix_count(offset, utf16: true)
end

#utf16_point_at(offset) ⇒ Denebola::Point

Parameters:

  • offset (Integer)

Returns:

  • (Denebola::Point)


85
86
87
88
# File 'lib/canopus/lazy_rope.rb', line 85

def utf16_point_at(offset)
  point = point_at(offset)
  Denebola::Point.new(point.row, prefix_count(offset, utf16: true) - prefix_count(line_start(point.row), utf16: true))
end