Module: Canopus::Workspace::GitAware
- Defined in:
- lib/canopus/workspace/git_aware.rb
Instance Method Summary collapse
- #checkout_branch(name) ⇒ Object
- #git ⇒ Object
- #git_decorations(buffer, rows) ⇒ Object
- #git_diff(buffer = editor.buffer, async: true) ⇒ Object
- #git_gutter_marks(buffer = editor.buffer) ⇒ Object
- #git_hunks(buffer = editor.buffer, async: true) ⇒ Object
- #git_relative_path(buffer = editor.buffer) ⇒ Object
- #git_status ⇒ Object
- #invalidate_git ⇒ Object
- #poll_git_changes(now: Process.clock_gettime(Process::CLOCK_MONOTONIC)) ⇒ Object
- #revert_current_hunk ⇒ Object
- #show_git_blame ⇒ Object
- #show_git_diff ⇒ Object
- #toggle_git_hunk(current = editor, row: nil) ⇒ Object
Instance Method Details
#checkout_branch(name) ⇒ Object
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.}" } 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
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.}" } 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.}" } unless @closed end end nil end |
#revert_current_hunk ⇒ Object
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.} #{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
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) = 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) = true end @window&.request_frame end |