Class: Ibex::Runtime::CST::SourceText

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/runtime/cst/source_text.rb,
sig/ibex/runtime/cst/source_text.rbs

Overview

Immutable byte-oriented source with lazy line/column conversion.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(text, file: nil) ⇒ SourceText

Returns a new instance of SourceText.

RBS:

  • (String text, ?file: String?) -> void

Parameters:

  • text (String)
  • file: (String, nil) (defaults to: nil)


15
16
17
18
19
20
# File 'lib/ibex/runtime/cst/source_text.rb', line 15

def initialize(text, file: nil)
  @text = text.encoding == Encoding::BINARY && text.frozen? ? text : text.b.freeze
  @file = file&.then { |value| value.frozen? ? value : value.dup.freeze }
  @line_starts = build_line_starts.freeze #: Array[Integer]
  freeze
end

Instance Attribute Details

#fileString? (readonly)

Signature:

  • String?

Returns:

  • (String, nil)


12
13
14
# File 'lib/ibex/runtime/cst/source_text.rb', line 12

def file
  @file
end

#textString (readonly)

RBS:

  • type position = [Integer, Integer]

Returns:

  • (String)


11
12
13
# File 'lib/ibex/runtime/cst/source_text.rb', line 11

def text
  @text
end

Instance Method Details

#apply(edits) ⇒ SourceText

Apply non-overlapping byte edits expressed against this source.

RBS:

  • (Array[TextEdit] edits) -> SourceText

Parameters:

Returns:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/ibex/runtime/cst/source_text.rb', line 27

def apply(edits)
  ordered = TextEdit.normalize(edits)
  output = String.new(encoding: Encoding::BINARY)
  cursor = 0
  ordered.each do |edit|
    raise ArgumentError, "text edits overlap" if edit.start < cursor
    raise RangeError, "text edit exceeds source" if edit.range.end > @text.bytesize

    output << (@text.byteslice(cursor, edit.start - cursor) || "".b)
    output << edit.insert_text
    cursor = edit.range.end
  end
  output << (@text.byteslice(cursor, @text.bytesize - cursor) || "".b)
  SourceText.new(output, file: @file)
end

#build_line_startsArray[Integer]

RBS:

  • () -> Array[Integer]

Returns:

  • (Array[Integer])


80
81
82
83
84
# File 'lib/ibex/runtime/cst/source_text.rb', line 80

def build_line_starts
  starts = [0]
  @text.bytes.each_with_index { |byte, index| starts << (index + 1) if byte == 10 }
  starts
end

#bytesizeInteger

RBS:

  • () -> Integer

Returns:

  • (Integer)


23
# File 'lib/ibex/runtime/cst/source_text.rb', line 23

def bytesize = @text.bytesize

#line_text(line) ⇒ String

RBS:

  • (Integer line) -> String

Parameters:

  • line (Integer)

Returns:

  • (String)


68
69
70
71
72
73
74
75
# File 'lib/ibex/runtime/cst/source_text.rb', line 68

def line_text(line)
  raise ArgumentError, "line must be positive" unless line.positive?

  start_offset = @line_starts.fetch(line - 1)
  end_offset = @line_starts.fetch(line, @text.bytesize)
  value = @text.byteslice(start_offset, end_offset - start_offset) || "".b
  value.delete_suffix("\n".b).delete_suffix("\r".b)
end

#location(range) ⇒ Ibex::Location

RBS:

  • (Range[Integer] range) -> Ibex::Location

Parameters:

  • range (Range[Integer])

Returns:

  • (Ibex::Location)


57
58
59
60
61
62
63
64
65
# File 'lib/ibex/runtime/cst/source_text.rb', line 57

def location(range)
  start_offset, end_offset = normalized_range(range)
  line, column = position(start_offset)
  end_line, end_column = position(end_offset)
  Ibex::Location.new(
    file: @file, line: line, column: column, end_line: end_line, end_column: end_column,
    start_byte: start_offset, end_byte: end_offset, source_line: line_text(line)
  )
end

#normalized_range(range) ⇒ position

RBS:

  • (Range[Integer] range) -> position

Parameters:

  • range (Range[Integer])

Returns:



94
95
96
97
98
99
100
101
102
103
# File 'lib/ibex/runtime/cst/source_text.rb', line 94

def normalized_range(range)
  start_offset = range.begin
  end_offset = range.end + (range.exclude_end? ? 0 : 1)
  validate_offset(start_offset)
  validate_offset(end_offset)
  raise RangeError, "range end precedes start" if end_offset < start_offset

  value = [start_offset, end_offset] #: position
  value.freeze
end

#position(offset) ⇒ position

Return one-based line and Unicode-scalar column for a byte offset. Invalid UTF-8 bytes count as one replacement scalar each.

RBS:

  • (Integer offset) -> position

Parameters:

  • offset (Integer)

Returns:



46
47
48
49
50
51
52
53
54
# File 'lib/ibex/runtime/cst/source_text.rb', line 46

def position(offset)
  validate_offset(offset)
  line_index = @line_starts.bsearch_index { |start| start > offset }
  line_index = line_index ? line_index - 1 : @line_starts.length - 1
  line_start = @line_starts.fetch(line_index)
  prefix = @text.byteslice(line_start, offset - line_start) || "".b
  value = [line_index + 1, unicode_scalar_length(prefix) + 1] #: position
  value.freeze
end

#unicode_scalar_length(bytes) ⇒ Integer

RBS:

  • (String bytes) -> Integer

Parameters:

  • bytes (String)

Returns:

  • (Integer)


106
107
108
# File 'lib/ibex/runtime/cst/source_text.rb', line 106

def unicode_scalar_length(bytes)
  bytes.dup.force_encoding(Encoding::UTF_8).scrub.length
end

#validate_offset(offset) ⇒ void

This method returns an undefined value.

RBS:

  • (Integer offset) -> void

Parameters:

  • offset (Integer)


87
88
89
90
91
# File 'lib/ibex/runtime/cst/source_text.rb', line 87

def validate_offset(offset)
  return if offset.between?(0, @text.bytesize)

  raise RangeError, "source offset #{offset} is outside 0..#{@text.bytesize}"
end