canopus 0.4.0 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
module Workspace::GitConflictResolution
|
|
5
|
+
MERGE_MAX_BYTES = 1 << 20
|
|
6
|
+
MERGE_MAX_LINES = 2_000
|
|
7
|
+
CONFLICT_MODES = [0o100644, 0o100755, 0o120000].freeze
|
|
8
|
+
CONFLICT_CHOICES = %i[ours theirs both manual].freeze
|
|
9
|
+
|
|
10
|
+
GitConflictContent = Data.define(:raw, :text, :encoding, :bom, :mode)
|
|
11
|
+
GitConflictCapture = Data.define(:conflict, :contents)
|
|
12
|
+
GitConflictPrepared = Data.define(:capture, :result, :reason)
|
|
13
|
+
GitConflictView = Data.define(:session, :role, :region)
|
|
14
|
+
GitConflictSession = Struct.new(:prepared, :result, :panes, :buffers, :editors, :choices, :mode,
|
|
15
|
+
keyword_init: true)
|
|
16
|
+
|
|
17
|
+
def show_git_conflicts(path = nil, async: !!@window)
|
|
18
|
+
state = git_state
|
|
19
|
+
raise Error, "Not a Git repository" unless state
|
|
20
|
+
raise Error, "Conflict resolution already in progress" if @git_conflict_resolve_job
|
|
21
|
+
path = safe_git_diff_path(path) if path
|
|
22
|
+
raise Error, "Finish the current manual conflict edit first" if git_conflict_editing?
|
|
23
|
+
|
|
24
|
+
generation = @git_conflict_generation = (@git_conflict_generation || 0) + 1
|
|
25
|
+
work = -> { prepare_git_conflict(capture_git_conflict(state, path)) }
|
|
26
|
+
return install_git_conflict(work.call) unless @window && async
|
|
27
|
+
|
|
28
|
+
@message = "Loading merge conflict…"
|
|
29
|
+
worker = Thread.new do
|
|
30
|
+
current = Thread.current
|
|
31
|
+
prepared = work.call
|
|
32
|
+
post { finish_git_conflict_load(current, generation, prepared, nil) } unless @closed
|
|
33
|
+
rescue StandardError => error
|
|
34
|
+
post { finish_git_conflict_load(current, generation, nil, error) } unless @closed
|
|
35
|
+
end
|
|
36
|
+
(@git_conflict_load_jobs ||= []) << worker
|
|
37
|
+
worker
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias_method :show_git_conflict, :show_git_conflicts
|
|
41
|
+
|
|
42
|
+
def resolve_git_conflict(choice, content: nil, mode: nil, async: !!@window, expected: nil)
|
|
43
|
+
raise ArgumentError, "invalid conflict choice" unless CONFLICT_CHOICES.include?(choice)
|
|
44
|
+
raise ArgumentError, "content is only accepted for manual resolution" if content && choice != :manual
|
|
45
|
+
if mode && !%i[both manual].include?(choice)
|
|
46
|
+
raise ArgumentError, "mode is only accepted for both or manual resolution"
|
|
47
|
+
end
|
|
48
|
+
session = @git_conflict_session
|
|
49
|
+
raise Error, "Open a merge conflict first" unless session
|
|
50
|
+
raise Error, "Merge conflict view is stale" if expected && !expected.equal?(session)
|
|
51
|
+
raise Error, "Conflict resolution already in progress" if @git_conflict_resolve_job
|
|
52
|
+
if session.prepared.reason && !%i[ours theirs].include?(choice)
|
|
53
|
+
raise Error, "#{session.prepared.reason}; choose ours or theirs"
|
|
54
|
+
end
|
|
55
|
+
unless mode.nil? || CONFLICT_MODES.include?(mode)
|
|
56
|
+
raise ArgumentError, "invalid conflict resolution mode"
|
|
57
|
+
end
|
|
58
|
+
if session.mode && mode && session.mode != mode
|
|
59
|
+
raise Error, "Conflict resolution mode changed"
|
|
60
|
+
end
|
|
61
|
+
session.mode ||= mode
|
|
62
|
+
|
|
63
|
+
if session.result&.conflicts&.any?
|
|
64
|
+
region = session.result.regions.first
|
|
65
|
+
resolution = case choice
|
|
66
|
+
when :ours then :ours
|
|
67
|
+
when :theirs then :theirs
|
|
68
|
+
when :both then :ours_then_theirs
|
|
69
|
+
else
|
|
70
|
+
content = session.buffers.fetch(:ours).text if content.nil?
|
|
71
|
+
validate_manual_conflict_text(content)
|
|
72
|
+
end
|
|
73
|
+
resolved = Porrima::Merge.resolve(session.result, region.index, resolution)
|
|
74
|
+
unless resolved.resolved?
|
|
75
|
+
session.result = resolved
|
|
76
|
+
session.choices << choice
|
|
77
|
+
install_git_conflict_region(session)
|
|
78
|
+
return session.editors.fetch(:ours)
|
|
79
|
+
end
|
|
80
|
+
content = Porrima::Merge.to_resolved_text(resolved)
|
|
81
|
+
elsif session.result
|
|
82
|
+
content = choice == :manual ? (content || session.buffers.fetch(:ours).text) :
|
|
83
|
+
Porrima::Merge.to_resolved_text(session.result)
|
|
84
|
+
elsif choice == :manual
|
|
85
|
+
raise Error, "Manual resolution requires content" unless content.is_a?(String)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
arguments = git_conflict_resolution_arguments(session, choice, content, session.mode)
|
|
89
|
+
schedule_git_conflict_resolution(session, arguments, async: async)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def git_conflict_decorations(buffer, rows)
|
|
93
|
+
view = buffer.instance_variable_get(:@git_conflict_view)
|
|
94
|
+
return [] unless view && view.session.equal?(@git_conflict_session)
|
|
95
|
+
|
|
96
|
+
first = [rows.begin, 0].max
|
|
97
|
+
last = [rows.exclude_end? ? rows.end : rows.end + 1, buffer.line_count].min
|
|
98
|
+
items = (first...last).map do |row|
|
|
99
|
+
Decoration::Item.new(:line, nil, row, nil, {color: "#f9758326"}.freeze,
|
|
100
|
+
10, :merge_conflict, nil)
|
|
101
|
+
end
|
|
102
|
+
return items unless view.role == :ours && rows.cover?(0)
|
|
103
|
+
|
|
104
|
+
choices = view.session.prepared.reason ? %i[ours theirs] : CONFLICT_CHOICES
|
|
105
|
+
choices.each_with_index do |choice, index|
|
|
106
|
+
label = {ours: "Use ours", theirs: "Use theirs", both: "Use both", manual: "Use manual edit"}.fetch(choice)
|
|
107
|
+
click = lambda do |_current, _offset|
|
|
108
|
+
resolve_git_conflict(choice, expected: view.session)
|
|
109
|
+
rescue StandardError => error
|
|
110
|
+
self.message = "Merge conflict: #{error.message}"
|
|
111
|
+
end
|
|
112
|
+
items << Decoration::Item.new(:block, nil, 0, label,
|
|
113
|
+
{height: 20, position: :above, color: :accent, padding_left: index * 8}.freeze,
|
|
114
|
+
20 + index, :merge_conflict, click)
|
|
115
|
+
end
|
|
116
|
+
items
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def close_git
|
|
120
|
+
@git_conflict_generation = (@git_conflict_generation || 0) + 1
|
|
121
|
+
jobs = Array(@git_conflict_load_jobs).compact.reject { |thread| thread.equal?(Thread.current) }
|
|
122
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.1
|
|
123
|
+
jobs.each do |thread|
|
|
124
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
125
|
+
break unless remaining.positive?
|
|
126
|
+
thread.join(remaining)
|
|
127
|
+
end
|
|
128
|
+
jobs.each { |thread| thread.kill if thread.alive? }
|
|
129
|
+
jobs.each(&:join)
|
|
130
|
+
@git_conflict_resolve_job&.join unless @git_conflict_resolve_job.equal?(Thread.current)
|
|
131
|
+
@git_conflict_load_jobs&.clear
|
|
132
|
+
@git_conflict_session = nil
|
|
133
|
+
super
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def capture_git_conflict(state, path = nil)
|
|
139
|
+
state.synchronize do |repository|
|
|
140
|
+
conflicts = repository.conflicts
|
|
141
|
+
conflict = path ? conflicts.find { |entry| entry.path == path } : conflicts.first
|
|
142
|
+
raise Error, "No merge conflict for #{path}" if path && !conflict
|
|
143
|
+
next unless conflict
|
|
144
|
+
|
|
145
|
+
contents = %i[base ours theirs].to_h do |role|
|
|
146
|
+
entry = conflict.public_send(role)
|
|
147
|
+
raw = if entry && entry.mode != 0o160000
|
|
148
|
+
type, value = repository.object(entry.oid)
|
|
149
|
+
raise Thuban::CorruptObject, "conflict entry is not a blob" unless type == "blob"
|
|
150
|
+
value.dup.freeze
|
|
151
|
+
end
|
|
152
|
+
[role, GitConflictContent.new(raw, nil, nil, nil, entry&.mode)]
|
|
153
|
+
end.freeze
|
|
154
|
+
GitConflictCapture.new(conflict, contents)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def prepare_git_conflict(capture)
|
|
159
|
+
return unless capture
|
|
160
|
+
|
|
161
|
+
raw_contents = capture.contents.values.filter_map(&:raw)
|
|
162
|
+
if raw_contents.sum(&:bytesize) > MERGE_MAX_BYTES
|
|
163
|
+
return GitConflictPrepared.new(capture, nil, "Conflict exceeds 1 MiB; choose a complete side".freeze)
|
|
164
|
+
end
|
|
165
|
+
decoded = capture.contents.transform_values do |content|
|
|
166
|
+
next content unless content.raw
|
|
167
|
+
text, encoding, bom = Buffer.decode_bytes(content.raw)
|
|
168
|
+
raise EncodingError unless content.raw == bom + text.encode(encoding).b
|
|
169
|
+
content.with(text: text.freeze, encoding: encoding, bom: bom.dup.freeze)
|
|
170
|
+
rescue EncodingError, Error
|
|
171
|
+
content
|
|
172
|
+
end.freeze
|
|
173
|
+
capture = capture.with(contents: decoded)
|
|
174
|
+
present = decoded.values.select(&:raw)
|
|
175
|
+
reason = if present.any? { |entry| entry.text.nil? }
|
|
176
|
+
"Binary or undecodable conflict; choose a complete side"
|
|
177
|
+
elsif present.sum { |entry| entry.text.count("\n") + 1 } > MERGE_MAX_LINES
|
|
178
|
+
"Conflict exceeds 2,000 lines; choose a complete side"
|
|
179
|
+
elsif present.any? { |entry| entry.text.match?(/\r(?!\n)|[\u2028\u2029]/) }
|
|
180
|
+
"Conflict uses unsupported line separators; choose a complete side"
|
|
181
|
+
elsif present.map { |entry| [entry.encoding, entry.bom] }.uniq.length > 1
|
|
182
|
+
"Conflict encodings differ; choose a complete side"
|
|
183
|
+
end
|
|
184
|
+
return GitConflictPrepared.new(capture, nil, reason.freeze) if reason
|
|
185
|
+
|
|
186
|
+
values = decoded.transform_values { |entry| entry.text || "" }
|
|
187
|
+
result = Porrima::Merge.three_way(base: values.fetch(:base), ours: values.fetch(:ours),
|
|
188
|
+
theirs: values.fetch(:theirs))
|
|
189
|
+
GitConflictPrepared.new(capture, result, nil)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def finish_git_conflict_load(worker, generation, prepared, error)
|
|
193
|
+
@git_conflict_load_jobs&.delete(worker)
|
|
194
|
+
return unless generation == @git_conflict_generation
|
|
195
|
+
return self.message = "Merge conflict: #{error.message}" if error
|
|
196
|
+
|
|
197
|
+
install_git_conflict(prepared)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def install_git_conflict(prepared, panes: nil)
|
|
201
|
+
unless prepared
|
|
202
|
+
cleanup_git_conflict_session(@git_conflict_session)
|
|
203
|
+
@git_conflict_session = nil
|
|
204
|
+
self.message = "No merge conflicts"
|
|
205
|
+
return nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
panes ||= @git_conflict_session&.panes
|
|
209
|
+
if @git_conflict_session && (!panes || panes.length != 3 || panes.uniq.length != 3 ||
|
|
210
|
+
panes.any? { |pane| !@panes.include?(pane) })
|
|
211
|
+
cleanup_git_conflict_session(@git_conflict_session)
|
|
212
|
+
panes = nil
|
|
213
|
+
else
|
|
214
|
+
cleanup_git_conflict_buffers(@git_conflict_session) if @git_conflict_session
|
|
215
|
+
end
|
|
216
|
+
panes ||= create_git_conflict_panes
|
|
217
|
+
session = GitConflictSession.new(prepared: prepared, result: prepared.result, panes: panes,
|
|
218
|
+
buffers: {}, editors: {}, choices: [], mode: nil)
|
|
219
|
+
@git_conflict_session = session
|
|
220
|
+
install_git_conflict_region(session)
|
|
221
|
+
self.message = "Resolve #{scm_display_path(prepared.capture.conflict.path)}"
|
|
222
|
+
session.editors.fetch(:ours)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def create_git_conflict_panes
|
|
226
|
+
panes = [@active_pane]
|
|
227
|
+
2.times do
|
|
228
|
+
focus(panes.last)
|
|
229
|
+
pane = split(:horizontal)
|
|
230
|
+
if (duplicate = pane.active)
|
|
231
|
+
pane.close(duplicate, discard: true)
|
|
232
|
+
release_buffer(duplicate.buffer, discard: true)
|
|
233
|
+
end
|
|
234
|
+
panes << pane
|
|
235
|
+
end
|
|
236
|
+
panes.freeze
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def install_git_conflict_region(session)
|
|
240
|
+
cleanup_git_conflict_buffers(session)
|
|
241
|
+
region = session.result&.regions&.first
|
|
242
|
+
conflict = region&.conflict
|
|
243
|
+
contents = if conflict
|
|
244
|
+
{base: conflict.base, ours: conflict.ours, theirs: conflict.theirs}
|
|
245
|
+
elsif session.result
|
|
246
|
+
captured = session.prepared.capture.contents
|
|
247
|
+
{base: captured.fetch(:base).text || "", ours: Porrima::Merge.to_resolved_text(session.result),
|
|
248
|
+
theirs: captured.fetch(:theirs).text || ""}
|
|
249
|
+
else
|
|
250
|
+
session.prepared.capture.contents.transform_values do |entry|
|
|
251
|
+
entry.text || (entry.raw ? "[binary content unavailable]\n" : "[deleted]\n")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
source = File.join(@root, session.prepared.capture.conflict.path)
|
|
255
|
+
definition = definition_for(source)
|
|
256
|
+
%i[base ours theirs].each_with_index do |role, index|
|
|
257
|
+
editable = role == :ours && (session.result || session.prepared.reason.nil?)
|
|
258
|
+
label = "#{scm_display_path(File.basename(source))} [#{role}]"
|
|
259
|
+
buffer = Buffer.new(contents.fetch(role), read_only: !editable)
|
|
260
|
+
buffer.instance_variable_set(:@display_name, label.freeze)
|
|
261
|
+
buffer.instance_variable_set(:@git_conflict_view, GitConflictView.new(session, role, region))
|
|
262
|
+
@buffers[buffer.object_id] = buffer
|
|
263
|
+
current = session.panes.fetch(index).open(buffer)
|
|
264
|
+
current.language = definition
|
|
265
|
+
apply_editor_settings(current)
|
|
266
|
+
session.buffers[role] = buffer
|
|
267
|
+
session.editors[role] = current
|
|
268
|
+
end
|
|
269
|
+
@decorations.invalidate(:merge_conflict)
|
|
270
|
+
focus(session.panes.fetch(1))
|
|
271
|
+
invalidate_hidden_selection_ranges
|
|
272
|
+
@window&.request_frame
|
|
273
|
+
session.editors.fetch(:ours)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def git_conflict_resolution_arguments(session, choice, content, mode)
|
|
277
|
+
capture = session.prepared.capture
|
|
278
|
+
return {choice: choice} unless session.result
|
|
279
|
+
choices = session.choices + [choice]
|
|
280
|
+
%i[ours theirs].each do |role|
|
|
281
|
+
side = capture.contents.fetch(role)
|
|
282
|
+
return {choice: role} if side.raw.nil? && content.empty? && choices.all?(role)
|
|
283
|
+
end
|
|
284
|
+
raw = encode_git_conflict_text(capture, content)
|
|
285
|
+
if %i[ours theirs].include?(choice) && choices.all?(choice)
|
|
286
|
+
selected = capture.contents.fetch(choice)
|
|
287
|
+
return {choice: choice} if selected.raw == raw && (mode.nil? || mode == selected.mode)
|
|
288
|
+
end
|
|
289
|
+
matching = %i[ours theirs].select do |role|
|
|
290
|
+
side = capture.contents.fetch(role)
|
|
291
|
+
side.raw == raw && (mode.nil? || mode == side.mode)
|
|
292
|
+
end
|
|
293
|
+
return {choice: matching.first} if matching.one?
|
|
294
|
+
selected_role = %i[ours theirs].find { |role| choices.all?(role) }
|
|
295
|
+
selected_side_mode = selected_role && capture.conflict.public_send(selected_role)&.mode
|
|
296
|
+
selected_mode = mode || (selected_side_mode if CONFLICT_MODES.include?(selected_side_mode)) ||
|
|
297
|
+
git_conflict_mode(capture.conflict)
|
|
298
|
+
{choice: :manual, content: raw, mode: selected_mode}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def encode_git_conflict_text(capture, text)
|
|
302
|
+
text = validate_manual_conflict_text(text)
|
|
303
|
+
codec = capture.contents.values.find(&:raw)
|
|
304
|
+
codec ? codec.bom + text.encode(codec.encoding).b : text.b
|
|
305
|
+
rescue EncodingError => error
|
|
306
|
+
raise Error, "Cannot encode conflict resolution: #{error.message}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def validate_manual_conflict_text(text)
|
|
310
|
+
raise Error, "Manual resolution requires UTF-8 text" unless text.is_a?(String) && text.valid_encoding? && !text.include?("\0")
|
|
311
|
+
text.encode(Encoding::UTF_8)
|
|
312
|
+
rescue EncodingError
|
|
313
|
+
raise Error, "Manual resolution requires UTF-8 text"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def git_conflict_mode(conflict)
|
|
317
|
+
modes = [conflict.ours, conflict.theirs].compact.map(&:mode).select { |value| CONFLICT_MODES.include?(value) }.uniq
|
|
318
|
+
raise Error, "Conflict resolution mode is ambiguous" unless modes.length == 1
|
|
319
|
+
modes.first
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def schedule_git_conflict_resolution(session, arguments, async:)
|
|
323
|
+
state = git_state
|
|
324
|
+
generation = @git_conflict_generation = (@git_conflict_generation || 0) + 1
|
|
325
|
+
work = lambda do
|
|
326
|
+
path = state.synchronize do |repository|
|
|
327
|
+
repository.resolve_conflict(session.prepared.capture.conflict, **arguments)
|
|
328
|
+
end
|
|
329
|
+
prepared = prepare_git_conflict(capture_git_conflict(state))
|
|
330
|
+
snapshot = state.capture rescue nil
|
|
331
|
+
[path, prepared, snapshot].freeze
|
|
332
|
+
end
|
|
333
|
+
set_git_conflict_buffer_locked(session, true)
|
|
334
|
+
unless @window && async
|
|
335
|
+
result = work.call
|
|
336
|
+
finish_git_conflict_resolution(session, generation, result, nil)
|
|
337
|
+
return result.first
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
self.message = "Resolving merge conflict…"
|
|
341
|
+
@git_conflict_resolve_job = Thread.new do
|
|
342
|
+
current = Thread.current
|
|
343
|
+
result = work.call
|
|
344
|
+
post { finish_git_conflict_resolution(session, generation, result, nil, current) } unless @closed
|
|
345
|
+
rescue StandardError => error
|
|
346
|
+
post { finish_git_conflict_resolution(session, generation, nil, error, current) } unless @closed
|
|
347
|
+
end
|
|
348
|
+
rescue StandardError => error
|
|
349
|
+
set_git_conflict_buffer_locked(session, false)
|
|
350
|
+
invalidate_git if error.is_a?(Thuban::RefLockError)
|
|
351
|
+
raise
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def finish_git_conflict_resolution(session, generation, result, error, worker = nil)
|
|
355
|
+
@git_conflict_resolve_job = nil if !worker || @git_conflict_resolve_job.equal?(worker)
|
|
356
|
+
return unless generation == @git_conflict_generation && session.equal?(@git_conflict_session)
|
|
357
|
+
if error
|
|
358
|
+
set_git_conflict_buffer_locked(session, false)
|
|
359
|
+
invalidate_git if error.is_a?(Thuban::RefLockError)
|
|
360
|
+
self.message = "Merge conflict: #{error.message}"
|
|
361
|
+
return
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
path, prepared, snapshot = result
|
|
365
|
+
panes = session.panes
|
|
366
|
+
invalidate_git
|
|
367
|
+
install_git_snapshot(git_state, snapshot) if snapshot
|
|
368
|
+
if prepared
|
|
369
|
+
install_git_conflict(prepared, panes: panes)
|
|
370
|
+
else
|
|
371
|
+
cleanup_git_conflict_session(session)
|
|
372
|
+
@git_conflict_session = nil
|
|
373
|
+
self.message = "Resolved #{scm_display_path(path)}; no merge conflicts remain"
|
|
374
|
+
end
|
|
375
|
+
path
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def cleanup_git_conflict_buffers(session)
|
|
379
|
+
return unless session
|
|
380
|
+
session.editors.to_a.each do |role, current|
|
|
381
|
+
pane = session.panes[%i[base ours theirs].index(role)]
|
|
382
|
+
pane.close(current, discard: true) if pane && pane.editors.include?(current)
|
|
383
|
+
release_buffer(current.buffer, discard: true)
|
|
384
|
+
end
|
|
385
|
+
session.buffers.clear
|
|
386
|
+
session.editors.clear
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def cleanup_git_conflict_session(session)
|
|
390
|
+
return unless session
|
|
391
|
+
cleanup_git_conflict_buffers(session)
|
|
392
|
+
session.panes.drop(1).reverse_each { |pane| close_empty_pane(pane) if @panes.include?(pane) && pane.editors.empty? }
|
|
393
|
+
focus(session.panes.first) if @panes.include?(session.panes.first)
|
|
394
|
+
@decorations.invalidate(:merge_conflict)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def git_conflict_editing?
|
|
398
|
+
session = @git_conflict_session
|
|
399
|
+
session && session.buffers.fetch(:ours, nil)&.dirty?
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def set_git_conflict_buffer_locked(session, locked)
|
|
403
|
+
buffer = session&.buffers&.fetch(:ours, nil)
|
|
404
|
+
return unless buffer && session.result
|
|
405
|
+
|
|
406
|
+
buffer.instance_variable_set(:@read_only, locked)
|
|
407
|
+
@window&.request_frame
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|