Class: Canopus::DisplayMap

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

Defined Under Namespace

Classes: LineBuilder, LineSet, OverlayMap, PendingLineSet, Row, Summary, Worker

Constant Summary collapse

BACKGROUND_THRESHOLD =
1 << 20
EMPTY_LINES =
Canopus::DisplayMap::LineSet.new([].freeze)
UNWRAPPED_LINE =
Canopus::DisplayMap::PendingLineSet.new(1)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffer, tab_size: 4, wrap_width: nil, background_threshold: BACKGROUND_THRESHOLD) ⇒ DisplayMap

Returns a new instance of DisplayMap.

Parameters:

  • buffer (Buffer)
  • tab_size: (Integer) (defaults to: 4)
  • wrap_width: (Integer, nil) (defaults to: nil)
  • background_threshold: (Integer, nil) (defaults to: BACKGROUND_THRESHOLD)

Raises:

  • (ArgumentError)


16
17
18
19
20
21
22
23
24
# File 'lib/canopus/display_map.rb', line 16

def initialize(buffer, tab_size: 4, wrap_width: nil, background_threshold: BACKGROUND_THRESHOLD)
  raise ArgumentError, "background threshold must be nonnegative or nil" unless background_threshold.nil? || (background_threshold.is_a?(Integer) && background_threshold >= 0)
  @background_threshold, @generation = background_threshold, 0
  @buffer, @rope = buffer, buffer.rope
  @fold_map, @overlay_map, @tab_map, @wrap_map, @block_map = FoldMap.new, OverlayMap.new,
    TabMap.new(tab_size: tab_size), WrapMap.new(width: wrap_width), BlockMap.new
  rebuild
  @subscription = buffer.on_edit { |patch| apply(patch) }
end

Instance Attribute Details

#block_map ⇒ BlockMap (readonly)

Returns the value of attribute block_map.

Returns:



13
14
15
# File 'lib/canopus/display_map.rb', line 13

def block_map
  @block_map
end

#fold_map ⇒ FoldMap (readonly)

Returns the value of attribute fold_map.

Returns:



13
14
15
# File 'lib/canopus/display_map.rb', line 13

def fold_map
  @fold_map
end

#layout_error ⇒ StandardError? (readonly)

Returns the value of attribute layout_error.

Returns:

  • (StandardError, nil)


14
15
16
# File 'lib/canopus/display_map.rb', line 14

def layout_error
  @layout_error
end

#overlay_map ⇒ OverlayMap (readonly)

Returns the value of attribute overlay_map.

Returns:



13
14
15
# File 'lib/canopus/display_map.rb', line 13

def overlay_map
  @overlay_map
end

#recomputed_lines ⇒ Integer (readonly)

Returns the value of attribute recomputed_lines.

Returns:

  • (Integer)


13
14
15
# File 'lib/canopus/display_map.rb', line 13

def recomputed_lines
  @recomputed_lines
end

#tab_map ⇒ TabMap (readonly)

Returns the value of attribute tab_map.

Returns:



13
14
15
# File 'lib/canopus/display_map.rb', line 13

def tab_map
  @tab_map
end

#tree ⇒ Denebola::Tree? (readonly)

Returns the value of attribute tree.

Returns:

  • (Denebola::Tree, nil)


13
14
15
# File 'lib/canopus/display_map.rb', line 13

def tree
  @tree
end

#wrap_map ⇒ WrapMap (readonly)

Returns the value of attribute wrap_map.

Returns:



13
14
15
# File 'lib/canopus/display_map.rb', line 13

def wrap_map
  @wrap_map
end

Instance Method Details

#dispose ⇒ void

This method returns an undefined value.



25
26
27
28
29
30
31
32
33
# File 'lib/canopus/display_map.rb', line 25

def dispose
  return if @disposed
  @disposed = true
  @layout_error = nil
  @subscription.detach
  stop_wrap(@wrap_map, @worker)
  @worker = @pending_result = nil
  nil
end

#each_row(range) ⇒ void #each_row(range) ⇒ Enumerator[[Row, Integer], void]

Overloads:

  • #each_row(range) ⇒ void

    This method returns an undefined value.

    Parameters:

    • range (Range[Integer])
  • #each_row(range) ⇒ Enumerator[[Row, Integer], void]

    Parameters:

    • range (Range[Integer])

    Returns:

    • (Enumerator[[Row, Integer], void])

Yields:

Yield Parameters:

  • arg0 (Row)
  • arg1 (Integer)

Yield Returns:

  • (void)


142
143
144
145
# File 'lib/canopus/display_map.rb', line 142

def each_row(range = 0...row_count)
  return enum_for(__method__, range) unless block_given?
  range.each { |i| yield row(i), i if i.between?(0, row_count - 1) }
end

#fold(range) ⇒ void

This method returns an undefined value.

Parameters:

  • range (Range[Integer])

Raises:



101
102
103
104
105
106
107
108
109
# File 'lib/canopus/display_map.rb', line 101

def fold(range)
  raise Error, "folding is disabled for large read-only files" if @lazy
  range = range.begin...(range.end + (range.exclude_end? ? 0 : 1))
  @rope.point_at(range.begin)
  @rope.point_at(range.end)
  first, last = affected_lines(@rope.point_at(range.begin).row, @rope.point_at(range.end).row)
  @fold_map.fold(range)
  replace_lines(first, last, last)
end

#insert_block(id, row:, text:, kind: :diagnostic) ⇒ void

This method returns an undefined value.

Parameters:

  • id (Object)
  • row: (Integer)
  • text: (String)
  • kind: (Symbol) (defaults to: :diagnostic)

Raises:



119
120
121
122
123
124
125
126
# File 'lib/canopus/display_map.rb', line 119

def insert_block(id, row:, text:, kind: :diagnostic)
  raise Error, "inline blocks are disabled for large read-only files" if @lazy
  raise RangeError, "block row outside buffer" unless row.between?(0, @rope.line_count - 1)
  previous = @block_map.blocks[id]
  @block_map.insert(id, row: row, text: text, kind: kind)
  replace_lines(previous.row, previous.row, previous.row) if previous && previous.row != row
  replace_lines(row, row, row)
end

#pending? ⇒ Boolean

Returns:

  • (Boolean)


34
# File 'lib/canopus/display_map.rb', line 34

def pending? = !!(!@disposed && !@failed && !@lazy && @background && (@computed_prefix < @tree.size || @tree.summary.pending_rows.positive?))

#poll(max_lines: 256) ⇒ Boolean

Call only from the foreground event loop, before painting. Read methods deliberately never install completed work halfway through a frame.

Parameters:

  • max_lines: (Integer) (defaults to: 256)

Returns:

  • (Boolean)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/canopus/display_map.rb', line 42

def poll(max_lines: 256)
  raise ArgumentError, "max_lines must be positive" unless max_lines.is_a?(Integer) && max_lines.positive?
  return false if @disposed || !@worker
  changed, consumed = false, 0
  while consumed < max_lines
    result = @pending_result || @worker.results.pop(true)
    @pending_result = nil
    next unless result.generation == @generation
    if result.error
      @failed = true
      @layout_error = result.error
      @worker.close
      @worker = nil
      raise result.error
    end
    take = [max_lines - consumed, result.lines.length].min
    values = result.lines.take(take)
    replace_values(result.first, take, values)
    @computed_prefix = [@computed_prefix, result.first + take].max if result.first <= @computed_prefix
    take.times { |index| @row_cache.delete(result.first + index) }
    @recomputed_lines += take
    consumed += take
    changed = true
    if take < result.lines.length
      @pending_result = result.with(first: result.first + take, lines: result.lines.drop(take).freeze)
    end
  end
  changed
rescue ThreadError
  changed
end

#progress ⇒ [Integer, Integer]

Returns:

  • ([Integer, Integer])


35
36
37
38
# File 'lib/canopus/display_map.rb', line 35

def progress
  return [@rope.line_count, @rope.line_count] if @lazy
  [@computed_prefix - @tree.prefix_summary(@computed_prefix).pending_rows, @tree.size]
end

#remove_block(id) ⇒ void

This method returns an undefined value.

Parameters:

  • id (Object)


127
128
129
130
# File 'lib/canopus/display_map.rb', line 127

def remove_block(id)
  block = @block_map.remove(id)
  replace_lines(block.row, block.row, block.row) if block
end

#row(index) ⇒ Row

Parameters:

  • index (Integer)

Returns:



131
132
133
134
135
136
137
138
139
140
# File 'lib/canopus/display_map.rb', line 131

def row(index)
  if @lazy
    raise RangeError, "display row outside buffer" unless index.between?(0, row_count - 1)
    @row_cache.shift if @row_cache.length >= 256
    return @row_cache[index] ||= build_lazy_row(index)
  end
  source, _lines, prefix = locate(index)
  lines = display_lines(source)
  lines.rows[index - prefix.display_rows]
end

#row_count ⇒ Integer

Returns:

  • (Integer)


73
# File 'lib/canopus/display_map.rb', line 73

def row_count = @lazy ? @rope.line_count : @tree.summary.display_rows

#set_overlays(items, font: nil, font_size: 14, line_height: 20) ⇒ Boolean

Parameters:

  • items (Array[Decoration::Item])
  • font: (Alhena::Font, nil) (defaults to: nil)
  • font_size: (Numeric) (defaults to: 14)
  • line_height: (Numeric) (defaults to: 20)

Returns:

  • (Boolean)


90
91
92
93
94
95
96
97
98
99
100
# File 'lib/canopus/display_map.rb', line 90

def set_overlays(items, font: nil, font_size: 14, line_height: 20)
  affected = @overlay_map.replace(items, rope: @rope, font: font, font_size: font_size, line_height: line_height)
  return false if affected.empty?

  @recomputed_lines = 0
  affected.select { |row| row.between?(0, @rope.line_count - 1) }
    .flat_map { |row| first, last = affected_lines(row, row); (first..last).to_a }.uniq.sort.each do |row|
    replace_lines(row, row, row)
  end
  true
end

#source_row(index) ⇒ Integer

Parameters:

  • index (Integer)

Returns:

  • (Integer)


141
# File 'lib/canopus/display_map.rb', line 141

def source_row(index) = @lazy ? index : locate(index)[0]

#tab_size=(size) ⇒ void

This method returns an undefined value.

Parameters:

  • size (Integer)


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

def tab_size=(size)
  return if size == @tab_map.tab_size
  @tab_map = TabMap.new(tab_size: size)
  rebuild(reuse: true)
end

#to_buffer(point, bias: :left) ⇒ Integer

Parameters:

Returns:

  • (Integer)


146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/canopus/display_map.rb', line 146

def to_buffer(point, bias: :left)
  if @lazy
    source = point.row.clamp(0, row_count - 1)
    line = row(source)
    return @rope.line_start(source) + line.offsets[point.column.clamp(0, line.offsets.length - 1)]
  end
  row = point.row.clamp(0, row_count - 1)
  source, _lines, prefix = locate(row)
  lines = display_lines(source)
  line = lines.rows[row - prefix.display_rows]
  column = point.column.clamp(0, line.offsets.length - 1)
  local = line.offsets[column]
  @rope.line_start(source) + local
end

#to_display(offset, bias: :left) ⇒ DisplayPoint

Parameters:

  • offset (Integer)
  • bias: (bias) (defaults to: :left)

Returns:



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/canopus/display_map.rb', line 160

def to_display(offset, bias: :left)
  if @lazy
    point = @rope.point_at(offset)
    local = offset - @rope.line_start(point.row)
    line = row(point.row)
    unless local.between?(line.offsets.first, line.offsets.last)
      line = @row_cache[point.row] = build_lazy_row(point.row, from: [local - 8192, 0].max)
    end
    column = line.offsets.bsearch_index { |position| position >= local } || line.text.length
    return DisplayPoint.new(point.row, column)
  end
  source = @rope.point_at(offset).row
  loop do
    start = @rope.line_start(source)
    folded = @fold_map.ranges.find { |range| range.begin < start && range.end >= start }
    break unless folded
    source = @rope.point_at(folded.begin).row
  end
  local = offset - @rope.line_start(source)
  lines = display_lines(source, local: local).rows
  prefix = @tree.prefix_summary(source).display_rows
  lines.each_with_index do |line, i|
    next unless line.kind == :text && line.&.any? do |placement|
      placement.align == :before && placement.item.range.begin == offset
    end

    column = line.offsets.bsearch_index { |position| position >= local } || line.offsets.length - 1
    return DisplayPoint.new(prefix + i, column)
  end
  lines.each_with_index do |line, i|
    next unless line.kind == :text
    next if local > line.offsets.last
    column = line.offsets.bsearch_index { |n| bias == :left ? n >= local : n > local }
    column = column ? (bias == :right ? [column - 1, 0].max : column) : line.offsets.length - 1
    return DisplayPoint.new(prefix + i, column)
  end
  DisplayPoint.new(prefix + lines.length - 1, lines.last&.text&.length || 0)
end

#unfold(offset) ⇒ void

This method returns an undefined value.

Parameters:

  • offset (Integer)


110
111
112
113
114
115
116
117
118
# File 'lib/canopus/display_map.rb', line 110

def unfold(offset)
  affected = @fold_map.ranges.select { |range| range.cover?(offset) }
  return if affected.empty?
  first = @rope.point_at(affected.map(&:begin).min).row
  last = @rope.point_at(affected.map(&:end).max).row
  first, last = affected_lines(first, last)
  @fold_map.unfold(offset)
  replace_lines(first, last, last)
end

#wrap_pixels(width, font: nil, font_size:, font_paths: nil, typesetter: nil) ⇒ void

This method returns an undefined value.

Parameters:

  • width (Numeric)
  • font: (Alhena::Font, nil) (defaults to: nil)
  • font_size: (Numeric)
  • font_paths: (Array[String], nil) (defaults to: nil)
  • typesetter: (Zaniah::TextSystem::Typesetter, nil) (defaults to: nil)

Raises:



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

def wrap_pixels(width, font: nil, font_size:, font_paths: nil, typesetter: nil)
  raise Error, "soft wrapping is disabled for large read-only files" if @lazy
  current = @wrap_map
  return if current.width == width && current.font.equal?(font || typesetter&.font) && current.font_size == font_size && current.font_paths == font_paths && current.typesetter.equal?(typesetter)
  replace_wrap(WrapMap.new(width: width, font: font, font_size: font_size, font_paths: font_paths, typesetter: typesetter))
end

#wrap_width=(width) ⇒ void

This method returns an undefined value.

Parameters:

  • width (Integer, nil)

Raises:



74
75
76
77
78
# File 'lib/canopus/display_map.rb', line 74

def wrap_width=(width)
  raise Error, "soft wrapping is disabled for large read-only files" if @lazy && width
  return if width == @wrap_map.width && !@wrap_map.font
  replace_wrap(WrapMap.new(width: width))
end