Class: Ibex::Runtime::CST::SourceText
- Inherits:
-
Object
- Object
- Ibex::Runtime::CST::SourceText
- 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
- #file ⇒ String? readonly
- #text ⇒ String readonly
Instance Method Summary collapse
-
#apply(edits) ⇒ SourceText
Apply non-overlapping byte edits expressed against this source.
- #build_line_starts ⇒ Array[Integer]
- #bytesize ⇒ Integer
-
#initialize(text, file: nil) ⇒ SourceText
constructor
A new instance of SourceText.
- #line_text(line) ⇒ String
- #location(range) ⇒ Ibex::Location
- #normalized_range(range) ⇒ position
-
#position(offset) ⇒ position
Return one-based line and Unicode-scalar column for a byte offset.
- #unicode_scalar_length(bytes) ⇒ Integer
- #validate_offset(offset) ⇒ void
Constructor Details
#initialize(text, file: nil) ⇒ SourceText
Returns a new instance of SourceText.
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
#file ⇒ String? (readonly)
12 13 14 |
# File 'lib/ibex/runtime/cst/source_text.rb', line 12 def file @file end |
#text ⇒ String (readonly)
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.
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_starts ⇒ 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 |
#bytesize ⇒ Integer
23 |
# File 'lib/ibex/runtime/cst/source_text.rb', line 23 def bytesize = @text.bytesize |
#line_text(line) ⇒ 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
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
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.
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
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.
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 |