Module: Canopus::Workspace::GitAware

Defined in:
lib/canopus/workspace/git_aware.rb

Instance Method Summary collapse

Instance Method Details

#checkout_branch(name) ⇒ Object

Raises:



159
160
161
162
163
164
165
166
167
# File 'lib/canopus/workspace/git_aware.rb', line 159

def checkout_branch(name)
  raise Error, "Save or discard all buffer changes before switching branches" if @buffers.values.any?(&:dirty?)
  raise Error, "Not a Git repository" unless git_state
  git_state.synchronize { |repository| repository.checkout(name) }
  invalidate_git
  refresh_files
  @buffers.values.each { |buffer| buffer.reload if buffer.path && File.file?(buffer.path) && !buffer.read_only }
  @message = "Switched to #{name}"
end

#git ⇒ Object



7
8
9
10
11
12
13
14
# File 'lib/canopus/workspace/git_aware.rb', line 7

def git
  return @git if defined?(@git)
  @git = Thuban::Repository.new(@root)
  @git = nil unless @git.root
  @git
rescue ArgumentError
  @git = nil
end

#git_decorations(buffer, rows) ⇒ Object



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

def git_decorations(buffer, rows)
  git_gutter_marks(buffer).filter_map do |mark|
    first = [mark.new_line, 1].max - 1
    count = mark.kind == :removed ? 1 : mark.count
    next if first + count <= rows.begin || first >= rows.end

    color = mark.kind == :removed ? :error : mark.kind == :added ? "#80b987" : :accent
    style = {color: color, rows: count, hit_width: 7}.freeze
    click = ->(current, row) { toggle_git_hunk(current, row: row) }
    Decoration::Item.new(:gutter, nil, first, "Toggle Git hunk", style, 0, :git, click)
  end
end

#git_diff(buffer = editor.buffer, async: true) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/canopus/workspace/git_aware.rb', line 65

def git_diff(buffer = editor.buffer, async: true)
  return unless git && buffer.path && buffer.path.start_with?(git.root + File::SEPARATOR) && buffer.rope.bytesize < 10 << 20
  path = git_relative_path(buffer)
  state = git_state
  key = [buffer.object_id, path, buffer.version, state.snapshot&.head]
  cached = state.cached_diff(key)
  return cached if cached
  if @window && async
    @git_diff_jobs ||= {}
    unless @git_diff_jobs[buffer]&.alive?
      snapshot = buffer.rope
      generation = @git_generation ||= 0
      @git_diff_jobs[buffer] = Thread.new do
        before = Buffer.decode_bytes(state.synchronize { |repository| repository.blob(path, reference: key.last || "HEAD") }.to_s).first
        diff = Porrima.diff(before, snapshot.to_s, context: 0)
        diff.hunks
        diff.marks
        post do
          if generation == @git_generation
            state.store_diff(key, diff)
            @decorations.invalidate(:git, buffer: buffer)
          end
        end unless @closed
      rescue StandardError => error
        post { @message = "Git diff: #{error.message}" } unless @closed
      end
    end
    nil
  else
    before = Buffer.decode_bytes(state.synchronize { |repository| repository.blob(path, reference: key.last || "HEAD") }.to_s).first
    diff = Porrima.diff(before, buffer.text, context: 0)
    diff.hunks
    diff.marks
    state.store_diff(key, diff)
  end
end

#git_gutter_marks(buffer = editor.buffer) ⇒ Object



102
# File 'lib/canopus/workspace/git_aware.rb', line 102

def git_gutter_marks(buffer = editor.buffer) = git_diff(buffer)&.marks || []

#git_hunks(buffer = editor.buffer, async: true) ⇒ Object



101
# File 'lib/canopus/workspace/git_aware.rb', line 101

def git_hunks(buffer = editor.buffer, async: true) = git_diff(buffer, async: async)&.hunks || []

#git_relative_path(buffer = editor.buffer) ⇒ Object

Raises:



61
62
63
64
# File 'lib/canopus/workspace/git_aware.rb', line 61

def git_relative_path(buffer = editor.buffer)
  raise Error, "Current document is not in a Git repository" unless git && buffer.path && buffer.path.start_with?(git.root + File::SEPARATOR)
  buffer.path.delete_prefix(git.root + File::SEPARATOR)
end

#git_status ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/canopus/workspace/git_aware.rb', line 15

def git_status
  state = git_state
  return {} unless state
  return state.snapshot.entries if state.snapshot
  if @window
    unless @git_status_job&.alive?
      generation = @git_generation ||= 0
      @git_status_job = Thread.new do
        snapshot = state.capture
        post { install_git_snapshot(state, snapshot) if generation == @git_generation } unless @closed
      rescue StandardError => error
        post { @message = "Git status: #{error.message}" } unless @closed
      end
    end
    {}
  else
    install_git_snapshot(state, state.capture).entries
  end
end

#invalidate_git ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
# File 'lib/canopus/workspace/git_aware.rb', line 34

def invalidate_git
  @git_generation = (@git_generation || 0) + 1
  @git_state&.invalidate
  @decorations.invalidate(:git)
  @panes.each do |pane|
    pane.editors.each do |current|
      current.display_map.block_map.blocks.values.each { |block| current.display_map.remove_block(block.id) if block.kind == :git_diff }
    end
  end
  nil
end

#poll_git_changes(now: Process.clock_gettime(Process::CLOCK_MONOTONIC)) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/canopus/workspace/git_aware.rb', line 45

def poll_git_changes(now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
  return if @last_git_poll && now - @last_git_poll < 1
  @last_git_poll = now
  state = git_state
  return unless state
  unless @git_poll_job&.alive?
    generation = @git_generation ||= 0
    @git_poll_job = Thread.new do
      identity = state.identity
      post { install_git_identity(identity) if generation == @git_generation } unless @closed
    rescue StandardError => error
      post { @message = "Git: #{error.message}" } unless @closed
    end
  end
  nil
end

#revert_current_hunk ⇒ Object

Raises:



152
153
154
155
156
157
158
# File 'lib/canopus/workspace/git_aware.rb', line 152

def revert_current_hunk
  row = editor.buffer.rope.point_at(editor.primary.head).row + 1
  hunk = git_diff(async: false)&.hunk_at(new_line: row)
  raise Error, "No change at the cursor" unless hunk
  replacement = Porrima.revert(editor.buffer.text, hunk)
  editor.buffer.edit([[0...editor.buffer.rope.bytesize, replacement]], kind: :revert_hunk)
end

#show_git_blame ⇒ Object



147
148
149
150
151
# File 'lib/canopus/workspace/git_aware.rb', line 147

def show_git_blame
  path = git_relative_path
  lines = git_state.synchronize { |repository| repository.blame(path) }
  @hover_card = lines.map { |line| "#{line.commit.to_s[0, 8]} #{line.author} #{line.text}" }.join
end

#show_git_diff ⇒ Object



115
116
117
118
119
120
121
122
# File 'lib/canopus/workspace/git_aware.rb', line 115

def show_git_diff
  path = git_relative_path
  before = Buffer.decode_bytes(git_state.synchronize { |repository| repository.blob(path) }.to_s).first
  buffer = Buffer.new(Porrima.unified(before, editor.buffer.text, old_name: "a/#{path}", new_name: "b/#{path}"), read_only: true)
  @buffers[buffer.object_id] = buffer
  @active_pane.open(buffer).language = Language::Definition.new("diff", "diff", [], "", /\A\z/, /\A\z/, [])
  invalidate_hidden_selection_ranges
end

#toggle_git_hunk(current = editor, row: nil) ⇒ Object

Raises:



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/canopus/workspace/git_aware.rb', line 123

def toggle_git_hunk(current = editor, row: nil)
  row ||= current.buffer.rope.point_at(current.primary.head).row
  hunk = git_diff(current.buffer, async: false)&.hunk_at(new_line: row + 1)
  raise Error, "No change at the cursor" unless hunk
  id = [:git_hunk, current.buffer.path, current.buffer.version, git_state.snapshot&.head, hunk.old_start, hunk.new_start]
  map = current.display_map
  if map.block_map.blocks.key?(id)
    map.remove_block(id)
    expanded = false
  else
    # Keep inline previews bounded; the full diff remains available in its
    # own document for large hunks and unusually long source lines.
    lines = ["@@ -#{hunk.old_start},#{hunk.old_count} +#{hunk.new_start},#{hunk.new_count} @@"]
    hunk.edits.first(200).each do |edit|
      body = edit.text.delete_suffix("\n").delete_suffix("\r")
      lines << "#{edit.kind == :delete ? '-' : edit.kind == :insert ? '+' : ' '}#{body.slice(0, 500)}#{body.length > 500 ? '…' : ''}"
    end
    lines << "… #{hunk.edits.length - 200} more lines; open git.diff for the full change" if hunk.edits.length > 200
    map.insert_block(id, row: [hunk.new_start - 1, 0].max.clamp(0, current.buffer.line_count - 1), text: lines.join("\n"), kind: :git_diff)
    expanded = true
  end
  @window&.request_frame
  expanded
end