canopus 0.3.0 → 0.4.0

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.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Canopus::DisplayMap::OverlayMap
4
+ Inline = Data.define(:item, :offset, :width, :cell_width, :height, :align)
5
+ Block = Data.define(:item, :row, :height, :row_span, :position)
6
+
7
+ attr_reader :line_height
8
+
9
+ def initialize
10
+ @inlines, @inline_values, @blocks, @line_height, @input = {}.freeze, [].freeze, {}.freeze, 1, nil
11
+ end
12
+
13
+ def replace(items, rope:, font: nil, font_size: 14, line_height: 20)
14
+ raise ArgumentError, "decorations must be an array" unless items.is_a?(Array)
15
+ validate_size(font_size, "font size")
16
+ validate_size(line_height, "line height")
17
+ input = [items, font&.object_id, font_size, line_height]
18
+ return [] if @input == input
19
+
20
+ inlines, blocks = Hash.new { |hash, key| hash[key] = [] }, Hash.new { |hash, key| hash[key] = [] }
21
+ items.each do |item|
22
+ raise TypeError, "overlays must be decoration items" unless item.is_a?(Canopus::Decoration::Item)
23
+ next unless %i[inline block].include?(item.kind)
24
+
25
+ if item.kind == :inline
26
+ offset = item.range&.begin
27
+ raise ArgumentError, "inline decoration requires a range" unless offset.is_a?(Integer)
28
+ raise RangeError, "inline decoration is outside the buffer" unless offset.between?(0, rope.bytesize)
29
+ row = rope.point_at(offset).row
30
+ local = offset - rope.line_start(row)
31
+ unless Zaniah::Unicode.grapheme_boundary?(rope.line(row), local)
32
+ raise ArgumentError, "inline decoration splits a grapheme cluster"
33
+ end
34
+ style = item.style.is_a?(Hash) ? item.style : {}
35
+ align = style.fetch(:align, :after)
36
+ raise ArgumentError, "inline alignment must be before or after" unless %i[before after].include?(align)
37
+ text = item.content.is_a?(String) ? item.content : item.content.respond_to?(:text) ? item.content.text.to_s : item.content.to_s
38
+ text = text.encode(Encoding::UTF_8)
39
+ padding_left = numeric_style(style, :padding_left)
40
+ padding_right = numeric_style(style, :padding_right)
41
+ validate_size(padding_left, "inline left padding", allow_zero: true)
42
+ validate_size(padding_right, "inline right padding", allow_zero: true)
43
+ padding = padding_left + padding_right
44
+ width = numeric_style(style, :width, nil) || (font ? font.advance_width(text.codepoints, size: font_size) : Zaniah::Unicode.width(text) * font_size * 0.6)
45
+ width += padding
46
+ height = numeric_style(style, :height, line_height)
47
+ validate_size(width, "inline width")
48
+ validate_size(height, "inline height")
49
+ raise ArgumentError, "inline height must not exceed line height" if height > line_height
50
+ cells = numeric_style(style, :cells, Zaniah::Unicode.width(text))
51
+ validate_size(cells, "inline cell width", allow_zero: true)
52
+ inlines[row] << Inline.new(item, offset, width.to_f, cells.to_f, height.to_f, align)
53
+ else
54
+ row = item.row
55
+ raise ArgumentError, "block decoration requires a row" unless row.is_a?(Integer)
56
+ raise RangeError, "block decoration is outside the buffer" unless row.between?(0, rope.line_count - 1)
57
+ style = item.style.is_a?(Hash) ? item.style : {}
58
+ position = style.fetch(:position, :above)
59
+ raise ArgumentError, "block position must be above or below" unless %i[above below].include?(position)
60
+ height = numeric_style(style, :height, line_height)
61
+ validate_size(height, "block height")
62
+ blocks[row] << Block.new(item, row, height.to_f, (height.to_f / line_height).ceil, position)
63
+ end
64
+ end
65
+ inlines.each_value { |values| values.sort_by! { |value| [value.offset, value.item.priority] }.freeze }
66
+ blocks.each_value { |values| values.sort_by! { |value| value.item.priority }.freeze }
67
+ inlines, blocks = inlines.to_h.freeze, blocks.to_h.freeze
68
+ inline_values = inlines.values.flatten.sort_by(&:offset).freeze
69
+ return [] if @inlines == inlines && @blocks == blocks && @line_height == line_height
70
+
71
+ affected = (@inlines.keys | @blocks.keys | inlines.keys | blocks.keys).select do |row|
72
+ @inlines.fetch(row, nil) != inlines.fetch(row, nil) ||
73
+ @blocks.fetch(row, nil) != blocks.fetch(row, nil) || @line_height != line_height
74
+ end
75
+ @inlines, @inline_values, @blocks, @line_height, @input = inlines, inline_values, blocks, line_height, input
76
+ affected.sort
77
+ end
78
+
79
+ def transform(rope, row, text, offsets)
80
+ start = rope.line_start(row)
81
+ first, last = offsets.first + start, offsets.last + start
82
+ index = @inline_values.bsearch_index { |inline| inline.offset >= first }
83
+ return [] unless index
84
+ values = []
85
+ while (inline = @inline_values[index]) && inline.offset <= last
86
+ values << inline
87
+ index += 1
88
+ end
89
+ boundaries, byte = [0], 0
90
+ text.each_char { |character| boundaries << (byte += character.bytesize) }
91
+ values.filter_map do |inline|
92
+ local = inline.offset - start
93
+ column = offsets.bsearch_index { |offset| offset >= local } || offsets.length - 1
94
+ next unless offsets[column] == local
95
+ inline.with(offset: boundaries.fetch(column))
96
+ end.freeze
97
+ end
98
+
99
+ def blocks(row, position) = @blocks.fetch(row, []).select { |block| block.position == position }
100
+
101
+ def snapshot
102
+ copy = self.class.allocate
103
+ copy.instance_variable_set(:@inlines, @inlines)
104
+ copy.instance_variable_set(:@inline_values, @inline_values)
105
+ copy.instance_variable_set(:@blocks, @blocks)
106
+ copy.instance_variable_set(:@line_height, @line_height)
107
+ copy.instance_variable_set(:@input, nil)
108
+ copy.freeze
109
+ end
110
+
111
+ private
112
+
113
+ def numeric_style(style, key, default = 0)
114
+ value = style.fetch(key, default)
115
+ raise ArgumentError, "#{key} must be numeric" unless value.nil? || value.is_a?(Numeric)
116
+ value
117
+ end
118
+
119
+ def validate_size(value, name, allow_zero: false)
120
+ valid = value.is_a?(Numeric) && value.real? && value.finite? && (allow_zero ? value >= 0 : value.positive?)
121
+ raise ArgumentError, "#{name} must be #{allow_zero ? 'nonnegative' : 'positive'} and finite" unless valid
122
+ end
123
+ end
@@ -10,14 +10,15 @@ module Canopus
10
10
  class DisplayMap
11
11
  BACKGROUND_THRESHOLD = 1 << 20
12
12
  Row = Data.define(:text, :offsets, :kind, :metadata)
13
- attr_reader :fold_map, :tab_map, :wrap_map, :block_map, :tree, :recomputed_lines
13
+ attr_reader :fold_map, :overlay_map, :tab_map, :wrap_map, :block_map, :tree, :recomputed_lines
14
14
  attr_reader :layout_error
15
15
 
16
16
  def initialize(buffer, tab_size: 4, wrap_width: nil, background_threshold: BACKGROUND_THRESHOLD)
17
17
  raise ArgumentError, "background threshold must be nonnegative or nil" unless background_threshold.nil? || (background_threshold.is_a?(Integer) && background_threshold >= 0)
18
18
  @background_threshold, @generation = background_threshold, 0
19
19
  @buffer, @rope = buffer, buffer.rope
20
- @fold_map, @tab_map, @wrap_map, @block_map = FoldMap.new, TabMap.new(tab_size: tab_size), WrapMap.new(width: wrap_width), BlockMap.new
20
+ @fold_map, @overlay_map, @tab_map, @wrap_map, @block_map = FoldMap.new, OverlayMap.new,
21
+ TabMap.new(tab_size: tab_size), WrapMap.new(width: wrap_width), BlockMap.new
21
22
  rebuild
22
23
  @subscription = buffer.on_edit { |patch| apply(patch) }
23
24
  end
@@ -86,6 +87,17 @@ module Canopus
86
87
  @tab_map = TabMap.new(tab_size: size)
87
88
  rebuild(reuse: true)
88
89
  end
90
+ def set_overlays(items, font: nil, font_size: 14, line_height: 20)
91
+ affected = @overlay_map.replace(items, rope: @rope, font: font, font_size: font_size, line_height: line_height)
92
+ return false if affected.empty?
93
+
94
+ @recomputed_lines = 0
95
+ affected.select { |row| row.between?(0, @rope.line_count - 1) }
96
+ .flat_map { |row| first, last = affected_lines(row, row); (first..last).to_a }.uniq.sort.each do |row|
97
+ replace_lines(row, row, row)
98
+ end
99
+ true
100
+ end
89
101
  def fold(range)
90
102
  raise Error, "folding is disabled for large read-only files" if @lazy
91
103
  range = range.begin...(range.end + (range.exclude_end? ? 0 : 1))
@@ -166,6 +178,14 @@ module Canopus
166
178
  local = offset - @rope.line_start(source)
167
179
  lines = display_lines(source, local: local).rows
168
180
  prefix = @tree.prefix_summary(source).display_rows
181
+ lines.each_with_index do |line, i|
182
+ next unless line.kind == :text && line.metadata&.any? do |placement|
183
+ placement.align == :before && placement.item.range.begin == offset
184
+ end
185
+
186
+ column = line.offsets.bsearch_index { |position| position >= local } || line.offsets.length - 1
187
+ return DisplayPoint.new(prefix + i, column)
188
+ end
169
189
  lines.each_with_index do |line, i|
170
190
  next unless line.kind == :text
171
191
  next if local > line.offsets.last
@@ -261,7 +281,7 @@ module Canopus
261
281
  @worker = nil
262
282
  return
263
283
  end
264
- @builder = LineBuilder.new(@rope, @fold_map, @tab_map, @wrap_map, @block_map)
284
+ @builder = LineBuilder.new(@rope, @fold_map, @overlay_map, @tab_map, @wrap_map, @block_map)
265
285
  @background = background_layout?(@rope)
266
286
  unless reuse && @background && @tree && @tree.size == @rope.line_count
267
287
  values = @background ? @builder.pending_lines(0, @rope.line_count) : Array.new(@rope.line_count) { |row| build_line(row) }
@@ -276,7 +296,7 @@ module Canopus
276
296
  @pending_result = nil
277
297
  @failed = false
278
298
  @layout_error = nil
279
- @builder = LineBuilder.new(@rope, @fold_map, @tab_map, @wrap_map, @block_map)
299
+ @builder = LineBuilder.new(@rope, @fold_map, @overlay_map, @tab_map, @wrap_map, @block_map)
280
300
  replacement = @background ? @builder.pending_lines(first, new_last - first + 1) : Array.new(new_last - first + 1) { |i| build_line(first + i) }
281
301
  if first < @computed_prefix
282
302
  @computed_prefix = old_last + 1 >= @computed_prefix ? first : @computed_prefix + new_last - old_last
@@ -340,6 +360,7 @@ end
340
360
  require_relative "display_map/summary"
341
361
  require_relative "display_map/line_set"
342
362
  require_relative "display_map/pending_line_set"
363
+ require_relative "display_map/overlay_map"
343
364
 
344
365
  Canopus::DisplayMap::EMPTY_LINES = Canopus::DisplayMap::LineSet.new([].freeze)
345
366
  Canopus::DisplayMap::UNWRAPPED_LINE = Canopus::DisplayMap::PendingLineSet.new(1)
@@ -46,6 +46,7 @@ module Canopus
46
46
  raise Error, "missing semantic token legend" if semantic && (!semantic.is_a?(Hash) || !semantic["legend"].is_a?(Hash))
47
47
  Protocol.semantic_tokens([], legend: semantic["legend"]) if semantic.is_a?(Hash)
48
48
  notify("initialized", {})
49
+ reopen_documents if @reopening_documents
49
50
  @state = :running
50
51
  self
51
52
  rescue StandardError => error
@@ -279,21 +280,28 @@ module Canopus
279
280
  return if @restart_thread&.alive?
280
281
  previous = @transport
281
282
  @restart_thread = Thread.new do
282
- previous.close
283
- until @closing || @restarts >= 3
284
- @restarts += 1
285
- sleep(0.2 * @restarts)
286
- break if @closing
287
- begin
288
- start
289
- @documents.values.dup.each { |buffer, language, _| open_document(buffer, language_id: language) }
290
- break
291
- rescue StandardError => failure
292
- report_error(failure)
283
+ @reopening_documents = true
284
+ begin
285
+ previous.close
286
+ until @closing || @restarts >= 3
287
+ @restarts += 1
288
+ sleep(0.2 * @restarts)
289
+ break if @closing
290
+ begin
291
+ start
292
+ break
293
+ rescue StandardError => failure
294
+ report_error(failure)
295
+ end
293
296
  end
297
+ ensure
298
+ @reopening_documents = false
294
299
  end
295
300
  end
296
301
  end
302
+ def reopen_documents
303
+ @documents.values.dup.each { |buffer, language, _| open_document(buffer, language_id: language) }
304
+ end
297
305
  end
298
306
  end
299
307
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canopus
4
+ module Panel
5
+ Definition = Data.define(:id, :title, :icon, :dock, :build, :badge)
6
+
7
+ class Registry
8
+ DOCKS = %i[left right bottom].freeze
9
+
10
+ attr_reader :docks
11
+
12
+ def initialize(docks)
13
+ @docks = docks
14
+ @definitions, @states = {}, {}
15
+ end
16
+
17
+ def register(definition, visible: true, size: nil)
18
+ definition = normalize(definition)
19
+ raise ArgumentError, "panel visibility must be true or false" unless [true, false].include?(visible)
20
+ size ||= @docks.fetch(definition.dock)[:size]
21
+ raise ArgumentError, "invalid panel size" unless size.is_a?(Numeric) && size.finite? && size.positive?
22
+ @definitions[definition.id] = definition
23
+ @states[definition.id] ||= {visible: visible, size: size}
24
+ definition
25
+ end
26
+
27
+ def fetch(id) = @definitions.fetch(id.to_s)
28
+ def key?(id) = @definitions.key?(id.to_s)
29
+ def visible?(id)
30
+ definition = fetch(id)
31
+ !!@states.fetch(definition.id)[:visible]
32
+ end
33
+
34
+ def show(id)
35
+ definition = fetch(id)
36
+ state = @states.fetch(definition.id)
37
+ state[:visible] = true
38
+ dock = @docks.fetch(definition.dock)
39
+ dock[:size], dock[:visible] = state[:size], true
40
+ definition
41
+ end
42
+
43
+ def hide(id)
44
+ definition = fetch(id)
45
+ state = @states.fetch(definition.id)
46
+ state[:size], state[:visible] = @docks.fetch(definition.dock)[:size], false
47
+ @docks.fetch(definition.dock)[:visible] = false if active(definition.dock).empty?
48
+ definition
49
+ end
50
+
51
+ def toggle(id) = visible?(id) ? hide(id) : show(id)
52
+
53
+ def active(dock)
54
+ raise ArgumentError, "invalid dock side" unless DOCKS.include?(dock)
55
+ @definitions.each_value.select { |definition| definition.dock == dock && visible?(definition.id) }
56
+ end
57
+
58
+ def badge(id, value)
59
+ definition = fetch(id).with(badge: value)
60
+ @definitions[definition.id] = definition
61
+ end
62
+
63
+ def resize(dock, size)
64
+ raise ArgumentError, "invalid dock size" unless size.is_a?(Numeric) && size.finite? && size.positive?
65
+ @docks.fetch(dock)[:size] = size
66
+ active(dock).each { |definition| @states.fetch(definition.id)[:size] = size }
67
+ size
68
+ end
69
+
70
+ def state
71
+ @states.to_h { |id, value| [id, {"visible" => value[:visible], "size" => value[:size]}] }
72
+ end
73
+
74
+ def restore(state, docks: nil)
75
+ raise ArgumentError, "panel state must be an object" unless state.is_a?(Hash) && state.length <= 1_000
76
+ restored = state.each_with_object({}) do |(id, value), result|
77
+ id = id.to_s
78
+ valid = value.is_a?(Hash) && [true, false].include?(value["visible"]) &&
79
+ value["size"].is_a?(Numeric) && value["size"].finite? && value["size"].positive?
80
+ raise ArgumentError, "invalid panel state: #{id}" if @definitions.key?(id) && !valid
81
+ next unless valid
82
+ result[id] = {visible: value["visible"], size: value["size"]}
83
+ end
84
+ @docks.replace(docks.transform_values(&:dup)) if docks
85
+ @states.merge!(restored)
86
+ self
87
+ end
88
+
89
+ private
90
+
91
+ def normalize(definition)
92
+ raise ArgumentError, "panel definition required" unless definition.is_a?(Definition)
93
+ id, title, icon, dock, build, badge = definition.deconstruct
94
+ valid_id = (id.is_a?(String) || id.is_a?(Symbol)) && !id.to_s.empty? &&
95
+ !id.to_s.include?(":") && !id.to_s.match?(/[\x00-\x1f\x7f]/)
96
+ raise ArgumentError, "invalid panel id" unless valid_id
97
+ raise ArgumentError, "panel title must be a string" unless title.is_a?(String)
98
+ raise ArgumentError, "invalid dock side" unless DOCKS.include?(dock)
99
+ raise ArgumentError, "panel builder must be callable" unless build.respond_to?(:call)
100
+ Definition.new(id.to_s.dup.freeze, title.dup.freeze, icon, dock, build, badge)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -10,7 +10,12 @@ module Canopus
10
10
  "theme" => "Canopus Dark", "font_family" => nil, "icon_theme" => nil, "languages" => {}, "language_servers" => {},
11
11
  "tabs" => {"activate_on_close" => "history", "close_on_middle_click" => true, "close_empty_pane" => true,
12
12
  "reopen_history_limit" => 20, "confirm_on_close_dirty" => true}.freeze,
13
- "dock" => {"bottom" => {"size" => 280, "visible" => false}.freeze}.freeze,
13
+ "dock" => {"left" => {"size" => 220, "visible" => true}.freeze,
14
+ "right" => {"size" => 260, "visible" => false}.freeze,
15
+ "bottom" => {"size" => 280, "visible" => false}.freeze,
16
+ "panels" => {"explorer" => {"size" => 220, "visible" => true}.freeze,
17
+ "search" => {"size" => 220, "visible" => false}.freeze,
18
+ "terminal" => {"size" => 280, "visible" => false}.freeze}.freeze}.freeze,
14
19
  "terminal" => {"shell" => nil, "working_directory" => "project", "env" => {}.freeze, "scrollback_lines" => 10_000,
15
20
  "font_size" => nil, "line_height" => 1.2, "copy_on_select" => false, "blinking" => "terminal_controlled", "cursor_shape" => "block",
16
21
  "close_on_exit" => "clean", "confirm_close_running" => true, "confirm_multiline_paste" => true,
@@ -27,9 +32,17 @@ module Canopus
27
32
  "bindings" => {"type" => "object", "maxProperties" => 1024, "additionalProperties" => {"type" => ["string", "null"]}}}}},
28
33
  "theme" => {"type" => "string"}, "font_family" => {"type" => ["string", "null"]},
29
34
  "icon_theme" => {"type" => ["string", "null"]},
30
- "tabs" => {"type" => "object"}, "terminal" => {"type" => "object"}, "dock" => {"type" => "object"},
35
+ "tabs" => {"type" => "object"}, "terminal" => {"type" => "object"},
36
+ "dock" => {"type" => "object", "properties" => {
37
+ "left" => {"$ref" => "#/$defs/dock"}, "right" => {"$ref" => "#/$defs/dock"},
38
+ "bottom" => {"$ref" => "#/$defs/dock"}, "panels" => {"type" => "object", "maxProperties" => 1000,
39
+ "additionalProperties" => {"$ref" => "#/$defs/panel"}}}},
31
40
  "languages" => {"type" => "object", "additionalProperties" => {"$ref" => "#"}},
32
- "language_servers" => {"type" => "object"}}}.freeze
41
+ "language_servers" => {"type" => "object"}}, "$defs" => {
42
+ "dock" => {"type" => "object", "required" => %w[size visible], "properties" => {
43
+ "size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}},
44
+ "panel" => {"type" => "object", "required" => %w[size visible], "properties" => {
45
+ "size" => {"type" => "number", "exclusiveMinimum" => 0}, "visible" => {"type" => "boolean"}}}}}.freeze
33
46
  attr_reader :values, :errors, :layers
34
47
  def self.schema = SCHEMA
35
48
  def self.user_path
@@ -163,8 +176,19 @@ module Canopus
163
176
 
164
177
  def validate_dock!
165
178
  dock = @values["dock"]
166
- bottom = dock["bottom"] if dock.is_a?(Hash)
167
- raise Error, "invalid dock.bottom" unless bottom.is_a?(Hash) && bottom["size"].is_a?(Numeric) && bottom["size"].positive? && [true, false].include?(bottom["visible"])
179
+ raise Error, "dock must be an object" unless dock.is_a?(Hash)
180
+ %w[left right bottom].each { |side| validate_dock_state!(dock[side], "dock.#{side}") }
181
+ panels = dock["panels"]
182
+ raise Error, "dock.panels must be an object" unless panels.is_a?(Hash) && panels.length <= 1_000
183
+ panels.each do |id, state|
184
+ raise Error, "invalid panel id" unless id.is_a?(String) && id.bytesize.between?(1, 256)
185
+ validate_dock_state!(state, "dock.panels.#{id}")
186
+ end
187
+ end
188
+
189
+ def validate_dock_state!(state, name)
190
+ valid_size = state.is_a?(Hash) && state["size"].is_a?(Numeric) && state["size"].finite? && state["size"].positive?
191
+ raise Error, "invalid #{name}" unless valid_size && [true, false].include?(state["visible"])
168
192
  end
169
193
  end
170
194
  end
@@ -7,8 +7,9 @@ module Canopus
7
7
  raise ArgumentError, "tab size must be positive" unless tab_size.is_a?(Integer) && tab_size.positive?
8
8
  @tab_size = tab_size
9
9
  end
10
- def transform(text, offsets, checkpoint: nil)
11
- output, positions, column, i = +"", [offsets.first], 0, 0
10
+ def transform(text, offsets, checkpoint: nil, overlays: nil)
11
+ output, positions, column, i, input_byte = +"", [offsets.first], 0, 0, 0
12
+ boundaries = {0 => 0} if overlays
12
13
  text.each_grapheme_cluster do |char|
13
14
  checkpoint&.call if (i & 1023).zero?
14
15
  if char == "\t"
@@ -23,8 +24,12 @@ module Canopus
23
24
  column += char.ascii_only? ? char.length : Zaniah::Unicode.width(char)
24
25
  end
25
26
  i += char.length
27
+ input_byte += char.bytesize
28
+ boundaries[input_byte] = output.bytesize if boundaries
26
29
  end
27
- [output, positions]
30
+ return [output, positions] unless overlays
31
+
32
+ [output, positions, overlays.map { |overlay| overlay.with(offset: boundaries.fetch(overlay.offset)) }.freeze]
28
33
  end
29
34
  end
30
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canopus
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -30,6 +30,7 @@ module Canopus
30
30
  def invalidate_git
31
31
  @git_generation = (@git_generation || 0) + 1
32
32
  @git_status = @git_diff_cache = nil
33
+ @decorations.invalidate(:git)
33
34
  @panes.each do |pane|
34
35
  pane.editors.each do |current|
35
36
  current.display_map.block_map.blocks.values.each { |block| current.display_map.remove_block(block.id) if block.kind == :git_diff }
@@ -73,7 +74,10 @@ module Canopus
73
74
  diff = Porrima.diff(before, snapshot.to_s, context: 0)
74
75
  diff.hunks
75
76
  diff.marks
76
- post { (@git_diff_cache ||= {})[key] = diff } unless @closed
77
+ post do
78
+ (@git_diff_cache ||= {})[key] = diff
79
+ @decorations.invalidate(:git, buffer: buffer)
80
+ end unless @closed
77
81
  rescue StandardError => error
78
82
  post { @message = "Git diff: #{error.message}" } unless @closed
79
83
  end
@@ -89,6 +93,18 @@ module Canopus
89
93
  end
90
94
  def git_hunks(buffer = editor.buffer, async: true) = git_diff(buffer, async: async)&.hunks || []
91
95
  def git_gutter_marks(buffer = editor.buffer) = git_diff(buffer)&.marks || []
96
+ def git_decorations(buffer, rows)
97
+ git_gutter_marks(buffer).filter_map do |mark|
98
+ first = [mark.new_line, 1].max - 1
99
+ count = mark.kind == :removed ? 1 : mark.count
100
+ next if first + count <= rows.begin || first >= rows.end
101
+
102
+ color = mark.kind == :removed ? :error : mark.kind == :added ? "#80b987" : :accent
103
+ style = {color: color, rows: count}.freeze
104
+ click = ->(current, row) { toggle_git_hunk(current, row: row) }
105
+ Decoration::Item.new(:gutter, nil, first, "Toggle Git hunk", style, 0, :git, click)
106
+ end
107
+ end
92
108
  def show_git_diff
93
109
  path = git_relative_path
94
110
  before = Buffer.decode_bytes(git.blob(path).to_s).first
@@ -19,9 +19,9 @@ module Canopus
19
19
  id
20
20
  end
21
21
  state = {version: 2, root: @root, layout: encode_layout(@layout), recent_files: @recent_files || [],
22
- active_pane: @panes.index(@active_pane), docks: @docks,
22
+ active_pane: @panes.index(@active_pane), docks: @docks, panels: @panels.state,
23
23
  terminals: @terminals.map { |current| {cwd: current.vt.cwd || (current.respond_to?(:initial_cwd) ? current.initial_cwd : @root), title: @terminal_names[current]} },
24
- active_terminal: @active_terminal_index, terminal_visible: !!@terminal_visible,
24
+ active_terminal: @active_terminal_index, terminal_visible: terminal_visible,
25
25
  panes: @panes.map do |pane|
26
26
  {active: pane.active_index, tabs: pane.editors.map do |current|
27
27
  {buffer_id: record.call(current.buffer), cursor: current.primary.head, pinned: pane.pinned.include?(current),
@@ -133,6 +133,14 @@ module Canopus
133
133
  raise Error, "invalid session dock" unless value.is_a?(Hash) && [true, false].include?(value["visible"]) && value["size"].is_a?(Numeric) && value["size"].finite? && value["size"].positive?
134
134
  restored_docks[side.to_sym] = {visible: value["visible"], size: value["size"].clamp(40, 4000)}
135
135
  end
136
+ restored_panels = data.fetch("panels", {})
137
+ raise Error, "invalid session panels" unless restored_panels.is_a?(Hash) && restored_panels.length <= 1_000
138
+ restored_panels.each do |id, value|
139
+ next unless @panels.key?(id)
140
+ valid = value.is_a?(Hash) && [true, false].include?(value["visible"]) &&
141
+ value["size"].is_a?(Numeric) && value["size"].finite? && value["size"].positive?
142
+ raise Error, "invalid session panel" unless valid
143
+ end
136
144
  active = data.fetch("active_pane", 0)
137
145
  raise Error, "invalid active pane" unless active.is_a?(Integer) && active.between?(0, restored_panes.length - 1)
138
146
  terminal_records = validate_session_terminals(data) if @settings["terminal"]["restore_on_startup"]
@@ -140,7 +148,8 @@ module Canopus
140
148
  close_language_documents
141
149
  @panes.each { |pane| pane.editors.each(&:dispose) }
142
150
  @buffers.each_value(&:close)
143
- @panes, @buffers, @layout, @docks = restored_panes, restored_buffers, restored_layout, restored_docks
151
+ @panels.restore(restored_panels, docks: restored_docks)
152
+ @panes, @buffers, @layout = restored_panes, restored_buffers, restored_layout
144
153
  @active_pane = @panes[active]
145
154
  @recent_files = Array(data["recent_files"]).select { |item| item.is_a?(String) && File.file?(item) }.first(100)
146
155
  restore_terminals(data, terminal_records) if terminal_records
@@ -164,7 +173,7 @@ module Canopus
164
173
  end
165
174
 
166
175
  def restore_terminals(data, records)
167
- old, old_index, old_visible = @terminals, @active_terminal_index, @terminal_visible
176
+ old, old_index, old_visible = @terminals, @active_terminal_index, terminal_visible
168
177
  @terminals = []
169
178
  records.each do |record|
170
179
  cwd = File.directory?(record["cwd"]) ? record["cwd"] : @root
@@ -172,12 +181,13 @@ module Canopus
172
181
  rename_terminal(record["title"], created) if record["title"].is_a?(String)
173
182
  end
174
183
  @active_terminal_index = @terminals.empty? ? 0 : data.fetch("active_terminal", 0)
175
- @terminal_visible = !!data["terminal_visible"] && !@terminals.empty?
184
+ self.terminal_visible = !!data["terminal_visible"] && !@terminals.empty?
176
185
  old.each { |current| current.close if current.respond_to?(:close) }
177
186
  rescue StandardError
178
187
  @terminals.each { |current| current.close if current.respond_to?(:close) }
179
188
  @terminals = old
180
- @active_terminal_index, @terminal_visible = old_index, old_visible
189
+ @active_terminal_index = old_index
190
+ self.terminal_visible = old_visible
181
191
  @message = "Session restored; terminals could not start"
182
192
  end
183
193
  end