@involvex/fresh-editor 0.1.76 → 0.1.78
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.
- package/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- package/package.json +7 -2
|
@@ -0,0 +1,1810 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
const editor = getEditor();
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 3-Way Merge Conflict Resolution Plugin
|
|
7
|
+
*
|
|
8
|
+
* Provides an interactive merge conflict resolution interface with:
|
|
9
|
+
* - Automatic detection of git conflict markers when files are opened
|
|
10
|
+
* - Multi-panel UI showing OURS, THEIRS, and editable RESULT
|
|
11
|
+
* - Keyboard navigation between conflicts
|
|
12
|
+
* - One-key resolution (accept ours, theirs, or both)
|
|
13
|
+
* - git-mediate style auto-resolution for trivial conflicts
|
|
14
|
+
* - Visual highlighting with intra-line diffing
|
|
15
|
+
*
|
|
16
|
+
* Architecture: Plugin-based implementation following the spec in docs/MERGE.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Types and Interfaces
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
interface ConflictBlock {
|
|
24
|
+
/** Index of this conflict (0-based) */
|
|
25
|
+
index: number;
|
|
26
|
+
/** Byte offset where the conflict starts (<<<<<<< marker) */
|
|
27
|
+
startOffset: number;
|
|
28
|
+
/** Byte offset where the conflict ends (after >>>>>>> marker) */
|
|
29
|
+
endOffset: number;
|
|
30
|
+
/** Content from "ours" side (our branch) */
|
|
31
|
+
ours: string;
|
|
32
|
+
/** Content from "base" (common ancestor) - may be empty if no diff3 */
|
|
33
|
+
base: string;
|
|
34
|
+
/** Content from "theirs" side (incoming changes) */
|
|
35
|
+
theirs: string;
|
|
36
|
+
/** Whether this conflict has been resolved */
|
|
37
|
+
resolved: boolean;
|
|
38
|
+
/** Resolution type if resolved */
|
|
39
|
+
resolution?: "ours" | "theirs" | "both" | "manual";
|
|
40
|
+
/** The resolved content (if resolved) */
|
|
41
|
+
resolvedContent?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MergeState {
|
|
45
|
+
/** Whether merge mode is active */
|
|
46
|
+
isActive: boolean;
|
|
47
|
+
/** The original buffer ID (file with conflicts) */
|
|
48
|
+
sourceBufferId: number | null;
|
|
49
|
+
/** The original file path */
|
|
50
|
+
sourcePath: string | null;
|
|
51
|
+
/** Original file content (for abort) */
|
|
52
|
+
originalContent: string;
|
|
53
|
+
/** List of detected conflicts */
|
|
54
|
+
conflicts: ConflictBlock[];
|
|
55
|
+
/** Index of currently selected conflict */
|
|
56
|
+
selectedIndex: number;
|
|
57
|
+
/** The OURS panel buffer ID */
|
|
58
|
+
oursPanelId: number | null;
|
|
59
|
+
/** The THEIRS panel buffer ID */
|
|
60
|
+
theirsPanelId: number | null;
|
|
61
|
+
/** The RESULT panel buffer ID (editable) */
|
|
62
|
+
resultPanelId: number | null;
|
|
63
|
+
/** Split IDs for each panel */
|
|
64
|
+
oursSplitId: number | null;
|
|
65
|
+
theirsSplitId: number | null;
|
|
66
|
+
resultSplitId: number | null;
|
|
67
|
+
/** Content for OURS side */
|
|
68
|
+
oursContent: string;
|
|
69
|
+
/** Content for THEIRS side */
|
|
70
|
+
theirsContent: string;
|
|
71
|
+
/** Content for BASE side (common ancestor) */
|
|
72
|
+
baseContent: string;
|
|
73
|
+
/** Current result content */
|
|
74
|
+
resultContent: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// State Management
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
const mergeState: MergeState = {
|
|
82
|
+
isActive: false,
|
|
83
|
+
sourceBufferId: null,
|
|
84
|
+
sourcePath: null,
|
|
85
|
+
originalContent: "",
|
|
86
|
+
conflicts: [],
|
|
87
|
+
selectedIndex: 0,
|
|
88
|
+
oursPanelId: null,
|
|
89
|
+
theirsPanelId: null,
|
|
90
|
+
resultPanelId: null,
|
|
91
|
+
oursSplitId: null,
|
|
92
|
+
theirsSplitId: null,
|
|
93
|
+
resultSplitId: null,
|
|
94
|
+
oursContent: "",
|
|
95
|
+
theirsContent: "",
|
|
96
|
+
baseContent: "",
|
|
97
|
+
resultContent: "",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Color Definitions
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
const colors = {
|
|
105
|
+
// Panel headers
|
|
106
|
+
oursHeader: [100, 200, 255] as [number, number, number], // Cyan for OURS
|
|
107
|
+
theirsHeader: [255, 180, 100] as [number, number, number], // Orange for THEIRS
|
|
108
|
+
resultHeader: [150, 255, 150] as [number, number, number], // Green for RESULT
|
|
109
|
+
|
|
110
|
+
// Conflict highlighting
|
|
111
|
+
conflictOurs: [50, 80, 100] as [number, number, number], // Blue-tinted background
|
|
112
|
+
conflictTheirs: [100, 70, 50] as [number, number, number], // Orange-tinted background
|
|
113
|
+
conflictBase: [70, 70, 70] as [number, number, number], // Gray for base
|
|
114
|
+
|
|
115
|
+
// Intra-line diff colors
|
|
116
|
+
diffAdd: [50, 100, 50] as [number, number, number], // Green for additions
|
|
117
|
+
diffDel: [100, 50, 50] as [number, number, number], // Red for deletions
|
|
118
|
+
diffMod: [50, 50, 100] as [number, number, number], // Blue for modifications
|
|
119
|
+
|
|
120
|
+
// Selection
|
|
121
|
+
selected: [80, 80, 120] as [number, number, number], // Selection highlight
|
|
122
|
+
|
|
123
|
+
// Buttons/actions
|
|
124
|
+
button: [100, 149, 237] as [number, number, number], // Cornflower blue
|
|
125
|
+
resolved: [100, 200, 100] as [number, number, number], // Green for resolved
|
|
126
|
+
unresolved: [200, 100, 100] as [number, number, number], // Red for unresolved
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// Mode Definition
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
// Define merge-conflict mode with keybindings
|
|
134
|
+
// Inherits from "normal" so cursor movement (hjkl) works
|
|
135
|
+
// Uses ] and [ for conflict navigation to avoid overriding j/k
|
|
136
|
+
editor.defineMode(
|
|
137
|
+
"merge-conflict",
|
|
138
|
+
"normal", // inherit from normal mode for cursor movement
|
|
139
|
+
[
|
|
140
|
+
// Conflict navigation (use ] and [ to avoid overriding j/k cursor movement)
|
|
141
|
+
["]", "merge_next_conflict"],
|
|
142
|
+
["[", "merge_prev_conflict"],
|
|
143
|
+
// Also support n/p for navigation
|
|
144
|
+
["n", "merge_next_conflict"],
|
|
145
|
+
["p", "merge_prev_conflict"],
|
|
146
|
+
|
|
147
|
+
// Resolution actions
|
|
148
|
+
["u", "merge_use_ours"], // Use ours
|
|
149
|
+
["t", "merge_take_theirs"], // Take theirs
|
|
150
|
+
["b", "merge_use_both"], // Use both
|
|
151
|
+
|
|
152
|
+
// Completion
|
|
153
|
+
["s", "merge_save_and_exit"], // Save & exit
|
|
154
|
+
["q", "merge_abort"], // Abort
|
|
155
|
+
|
|
156
|
+
// Help
|
|
157
|
+
["?", "merge_show_help"],
|
|
158
|
+
],
|
|
159
|
+
true // read-only for navigation panels
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Define merge-result mode for the editable RESULT panel
|
|
163
|
+
editor.defineMode(
|
|
164
|
+
"merge-result",
|
|
165
|
+
"normal", // inherit from normal mode for editing
|
|
166
|
+
[
|
|
167
|
+
// Navigation - use C-j/C-k to avoid conflicting with C-p (command palette)
|
|
168
|
+
["C-j", "merge_next_conflict"],
|
|
169
|
+
["C-k", "merge_prev_conflict"],
|
|
170
|
+
|
|
171
|
+
// Resolution shortcuts
|
|
172
|
+
["C-u", "merge_use_ours"],
|
|
173
|
+
["C-t", "merge_take_theirs"],
|
|
174
|
+
["C-b", "merge_use_both"],
|
|
175
|
+
|
|
176
|
+
// Completion
|
|
177
|
+
["C-s", "merge_save_and_exit"],
|
|
178
|
+
["C-q", "merge_abort"],
|
|
179
|
+
],
|
|
180
|
+
false // editable
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Conflict Detection and Parsing
|
|
185
|
+
// =============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if content contains git conflict markers
|
|
189
|
+
*/
|
|
190
|
+
function hasConflictMarkers(content: string): boolean {
|
|
191
|
+
return content.includes("<<<<<<<") &&
|
|
192
|
+
content.includes("=======") &&
|
|
193
|
+
content.includes(">>>>>>>");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Parse conflict markers from file content
|
|
198
|
+
* Supports both 2-way (no base) and 3-way (with base via diff3) conflicts
|
|
199
|
+
*/
|
|
200
|
+
function parseConflicts(content: string): ConflictBlock[] {
|
|
201
|
+
const conflicts: ConflictBlock[] = [];
|
|
202
|
+
|
|
203
|
+
// Regex to match conflict blocks
|
|
204
|
+
// Supports optional base section (||||||| marker)
|
|
205
|
+
// Key: use ^ anchors to ensure markers are at start of lines (multiline mode)
|
|
206
|
+
// Note: use \r?\n to handle both LF and CRLF line endings
|
|
207
|
+
const conflictRegex = /^<<<<<<<[^\r\n]*\r?\n([\s\S]*?)(?:^\|\|\|\|\|\|\|[^\r\n]*\r?\n([\s\S]*?))?^=======\r?\n([\s\S]*?)^>>>>>>>[^\r\n]*$/gm;
|
|
208
|
+
|
|
209
|
+
let match;
|
|
210
|
+
let index = 0;
|
|
211
|
+
|
|
212
|
+
while ((match = conflictRegex.exec(content)) !== null) {
|
|
213
|
+
const startOffset = match.index;
|
|
214
|
+
const endOffset = match.index + match[0].length;
|
|
215
|
+
|
|
216
|
+
conflicts.push({
|
|
217
|
+
index: index++,
|
|
218
|
+
startOffset,
|
|
219
|
+
endOffset,
|
|
220
|
+
ours: match[1] || "",
|
|
221
|
+
base: match[2] || "",
|
|
222
|
+
theirs: match[3] || "",
|
|
223
|
+
resolved: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return conflicts;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Extract non-conflict sections and build initial result content
|
|
232
|
+
*/
|
|
233
|
+
function buildInitialResult(content: string, conflicts: ConflictBlock[]): string {
|
|
234
|
+
if (conflicts.length === 0) return content;
|
|
235
|
+
|
|
236
|
+
let result = "";
|
|
237
|
+
let lastEnd = 0;
|
|
238
|
+
|
|
239
|
+
for (const conflict of conflicts) {
|
|
240
|
+
// Add non-conflict text before this conflict
|
|
241
|
+
result += content.substring(lastEnd, conflict.startOffset);
|
|
242
|
+
|
|
243
|
+
// Add a placeholder for the conflict
|
|
244
|
+
result += `<<<CONFLICT_${conflict.index}>>>`;
|
|
245
|
+
|
|
246
|
+
lastEnd = conflict.endOffset;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add remaining text after last conflict
|
|
250
|
+
result += content.substring(lastEnd);
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// Git Data Fetching
|
|
257
|
+
// =============================================================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Fetch the base (common ancestor), ours, and theirs versions from git
|
|
261
|
+
*/
|
|
262
|
+
async function fetchGitVersions(filePath: string): Promise<{
|
|
263
|
+
base: string;
|
|
264
|
+
ours: string;
|
|
265
|
+
theirs: string;
|
|
266
|
+
} | null> {
|
|
267
|
+
try {
|
|
268
|
+
// Get the directory of the file for running git commands
|
|
269
|
+
const fileDir = editor.pathDirname(filePath);
|
|
270
|
+
|
|
271
|
+
// Get the git repository root
|
|
272
|
+
const repoRootResult = await editor.spawnProcess("git", [
|
|
273
|
+
"rev-parse", "--show-toplevel"
|
|
274
|
+
], fileDir);
|
|
275
|
+
|
|
276
|
+
if (repoRootResult.exit_code !== 0) {
|
|
277
|
+
editor.debug(`fetchGitVersions: failed to get repo root`);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const repoRoot = repoRootResult.stdout.trim();
|
|
282
|
+
|
|
283
|
+
// Compute the relative path from repo root to the file
|
|
284
|
+
// filePath is absolute, repoRoot is absolute
|
|
285
|
+
let relativePath = filePath;
|
|
286
|
+
if (filePath.startsWith(repoRoot + "/")) {
|
|
287
|
+
relativePath = filePath.substring(repoRoot.length + 1);
|
|
288
|
+
} else if (filePath.startsWith(repoRoot)) {
|
|
289
|
+
relativePath = filePath.substring(repoRoot.length);
|
|
290
|
+
if (relativePath.startsWith("/")) {
|
|
291
|
+
relativePath = relativePath.substring(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
editor.debug(`fetchGitVersions: repoRoot=${repoRoot}, relativePath=${relativePath}`);
|
|
296
|
+
|
|
297
|
+
// Get OURS version (--ours or :2:)
|
|
298
|
+
const oursResult = await editor.spawnProcess("git", [
|
|
299
|
+
"show", `:2:${relativePath}`
|
|
300
|
+
], fileDir);
|
|
301
|
+
editor.debug(`fetchGitVersions: ours exit_code=${oursResult.exit_code}, stdout length=${oursResult.stdout.length}`);
|
|
302
|
+
|
|
303
|
+
// Get THEIRS version (--theirs or :3:)
|
|
304
|
+
const theirsResult = await editor.spawnProcess("git", [
|
|
305
|
+
"show", `:3:${relativePath}`
|
|
306
|
+
], fileDir);
|
|
307
|
+
editor.debug(`fetchGitVersions: theirs exit_code=${theirsResult.exit_code}, stdout length=${theirsResult.stdout.length}`);
|
|
308
|
+
|
|
309
|
+
// Get BASE version (common ancestor, :1:)
|
|
310
|
+
const baseResult = await editor.spawnProcess("git", [
|
|
311
|
+
"show", `:1:${relativePath}`
|
|
312
|
+
], fileDir);
|
|
313
|
+
editor.debug(`fetchGitVersions: base exit_code=${baseResult.exit_code}, stdout length=${baseResult.stdout.length}`);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
base: baseResult.exit_code === 0 ? baseResult.stdout : "",
|
|
317
|
+
ours: oursResult.exit_code === 0 ? oursResult.stdout : "",
|
|
318
|
+
theirs: theirsResult.exit_code === 0 ? theirsResult.stdout : "",
|
|
319
|
+
};
|
|
320
|
+
} catch (e) {
|
|
321
|
+
editor.debug(`Failed to fetch git versions: ${e}`);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// =============================================================================
|
|
327
|
+
// Auto-Resolution (git-mediate style)
|
|
328
|
+
// =============================================================================
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Attempt to auto-resolve trivial conflicts using git-mediate logic
|
|
332
|
+
* A conflict is trivially resolvable if only one side changed from base
|
|
333
|
+
*/
|
|
334
|
+
function autoResolveConflicts(conflicts: ConflictBlock[]): void {
|
|
335
|
+
for (const conflict of conflicts) {
|
|
336
|
+
if (conflict.resolved) continue;
|
|
337
|
+
|
|
338
|
+
// If we have base content, check for trivial resolution
|
|
339
|
+
if (conflict.base) {
|
|
340
|
+
const oursChanged = conflict.ours.trim() !== conflict.base.trim();
|
|
341
|
+
const theirsChanged = conflict.theirs.trim() !== conflict.base.trim();
|
|
342
|
+
|
|
343
|
+
if (oursChanged && !theirsChanged) {
|
|
344
|
+
// Only ours changed - use ours
|
|
345
|
+
conflict.resolved = true;
|
|
346
|
+
conflict.resolution = "ours";
|
|
347
|
+
conflict.resolvedContent = conflict.ours;
|
|
348
|
+
editor.debug(`Auto-resolved conflict ${conflict.index}: using OURS (theirs unchanged)`);
|
|
349
|
+
} else if (!oursChanged && theirsChanged) {
|
|
350
|
+
// Only theirs changed - use theirs
|
|
351
|
+
conflict.resolved = true;
|
|
352
|
+
conflict.resolution = "theirs";
|
|
353
|
+
conflict.resolvedContent = conflict.theirs;
|
|
354
|
+
editor.debug(`Auto-resolved conflict ${conflict.index}: using THEIRS (ours unchanged)`);
|
|
355
|
+
} else if (!oursChanged && !theirsChanged) {
|
|
356
|
+
// Neither changed (identical) - use either
|
|
357
|
+
conflict.resolved = true;
|
|
358
|
+
conflict.resolution = "ours";
|
|
359
|
+
conflict.resolvedContent = conflict.ours;
|
|
360
|
+
editor.debug(`Auto-resolved conflict ${conflict.index}: both identical to base`);
|
|
361
|
+
}
|
|
362
|
+
// If both changed differently, leave unresolved
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check if ours and theirs are identical
|
|
366
|
+
if (!conflict.resolved && conflict.ours.trim() === conflict.theirs.trim()) {
|
|
367
|
+
conflict.resolved = true;
|
|
368
|
+
conflict.resolution = "ours";
|
|
369
|
+
conflict.resolvedContent = conflict.ours;
|
|
370
|
+
editor.debug(`Auto-resolved conflict ${conflict.index}: ours and theirs identical`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// Word-Level Diff
|
|
377
|
+
// =============================================================================
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Simple word-level diff for intra-line highlighting
|
|
381
|
+
*/
|
|
382
|
+
function computeWordDiff(a: string, b: string): Array<{
|
|
383
|
+
type: "same" | "add" | "del" | "mod";
|
|
384
|
+
aStart: number;
|
|
385
|
+
aEnd: number;
|
|
386
|
+
bStart: number;
|
|
387
|
+
bEnd: number;
|
|
388
|
+
}> {
|
|
389
|
+
// Split into words (preserving whitespace positions)
|
|
390
|
+
const aWords = a.split(/(\s+)/);
|
|
391
|
+
const bWords = b.split(/(\s+)/);
|
|
392
|
+
|
|
393
|
+
const diffs: Array<{
|
|
394
|
+
type: "same" | "add" | "del" | "mod";
|
|
395
|
+
aStart: number;
|
|
396
|
+
aEnd: number;
|
|
397
|
+
bStart: number;
|
|
398
|
+
bEnd: number;
|
|
399
|
+
}> = [];
|
|
400
|
+
|
|
401
|
+
let aPos = 0;
|
|
402
|
+
let bPos = 0;
|
|
403
|
+
let aIdx = 0;
|
|
404
|
+
let bIdx = 0;
|
|
405
|
+
|
|
406
|
+
// Simple LCS-based diff (for short texts)
|
|
407
|
+
while (aIdx < aWords.length || bIdx < bWords.length) {
|
|
408
|
+
if (aIdx >= aWords.length) {
|
|
409
|
+
// Rest of b is additions
|
|
410
|
+
const bWord = bWords[bIdx];
|
|
411
|
+
diffs.push({
|
|
412
|
+
type: "add",
|
|
413
|
+
aStart: aPos,
|
|
414
|
+
aEnd: aPos,
|
|
415
|
+
bStart: bPos,
|
|
416
|
+
bEnd: bPos + bWord.length,
|
|
417
|
+
});
|
|
418
|
+
bPos += bWord.length;
|
|
419
|
+
bIdx++;
|
|
420
|
+
} else if (bIdx >= bWords.length) {
|
|
421
|
+
// Rest of a is deletions
|
|
422
|
+
const aWord = aWords[aIdx];
|
|
423
|
+
diffs.push({
|
|
424
|
+
type: "del",
|
|
425
|
+
aStart: aPos,
|
|
426
|
+
aEnd: aPos + aWord.length,
|
|
427
|
+
bStart: bPos,
|
|
428
|
+
bEnd: bPos,
|
|
429
|
+
});
|
|
430
|
+
aPos += aWord.length;
|
|
431
|
+
aIdx++;
|
|
432
|
+
} else if (aWords[aIdx] === bWords[bIdx]) {
|
|
433
|
+
// Same
|
|
434
|
+
const word = aWords[aIdx];
|
|
435
|
+
diffs.push({
|
|
436
|
+
type: "same",
|
|
437
|
+
aStart: aPos,
|
|
438
|
+
aEnd: aPos + word.length,
|
|
439
|
+
bStart: bPos,
|
|
440
|
+
bEnd: bPos + word.length,
|
|
441
|
+
});
|
|
442
|
+
aPos += word.length;
|
|
443
|
+
bPos += word.length;
|
|
444
|
+
aIdx++;
|
|
445
|
+
bIdx++;
|
|
446
|
+
} else {
|
|
447
|
+
// Different - mark as modification
|
|
448
|
+
const aWord = aWords[aIdx];
|
|
449
|
+
const bWord = bWords[bIdx];
|
|
450
|
+
diffs.push({
|
|
451
|
+
type: "mod",
|
|
452
|
+
aStart: aPos,
|
|
453
|
+
aEnd: aPos + aWord.length,
|
|
454
|
+
bStart: bPos,
|
|
455
|
+
bEnd: bPos + bWord.length,
|
|
456
|
+
});
|
|
457
|
+
aPos += aWord.length;
|
|
458
|
+
bPos += bWord.length;
|
|
459
|
+
aIdx++;
|
|
460
|
+
bIdx++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return diffs;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// =============================================================================
|
|
468
|
+
// View Rendering - Full File Content (JetBrains-style)
|
|
469
|
+
// =============================================================================
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Build entries showing the full file content for OURS or THEIRS
|
|
473
|
+
* This displays the complete file from git, highlighting conflict regions
|
|
474
|
+
*/
|
|
475
|
+
function buildFullFileEntries(side: "ours" | "theirs"): TextPropertyEntry[] {
|
|
476
|
+
const entries: TextPropertyEntry[] = [];
|
|
477
|
+
const content = side === "ours" ? mergeState.oursContent : mergeState.theirsContent;
|
|
478
|
+
|
|
479
|
+
// If we don't have the git version, fall back to showing conflict regions only
|
|
480
|
+
if (!content) {
|
|
481
|
+
entries.push({
|
|
482
|
+
text: editor.t("panel.git_unavailable") + "\n\n",
|
|
483
|
+
properties: { type: "warning" },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Show conflict regions from parsed conflicts
|
|
487
|
+
for (const conflict of mergeState.conflicts) {
|
|
488
|
+
const conflictContent = side === "ours" ? conflict.ours : conflict.theirs;
|
|
489
|
+
const isSelected = conflict.index === mergeState.selectedIndex;
|
|
490
|
+
|
|
491
|
+
entries.push({
|
|
492
|
+
text: `--- ${editor.t("panel.conflict", { index: String(conflict.index + 1) })} ---\n`,
|
|
493
|
+
properties: {
|
|
494
|
+
type: "conflict-header",
|
|
495
|
+
conflictIndex: conflict.index,
|
|
496
|
+
selected: isSelected,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
entries.push({
|
|
501
|
+
text: (conflictContent || editor.t("panel.empty")) + "\n",
|
|
502
|
+
properties: {
|
|
503
|
+
type: "conflict-content",
|
|
504
|
+
conflictIndex: conflict.index,
|
|
505
|
+
side: side,
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return entries;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Show full file content with conflict regions highlighted
|
|
513
|
+
// The content from git is the clean version without markers
|
|
514
|
+
const lines = content.split("\n");
|
|
515
|
+
for (let i = 0; i < lines.length; i++) {
|
|
516
|
+
const line = lines[i];
|
|
517
|
+
// Check if this line is in a conflict region
|
|
518
|
+
const inConflict = isLineInConflict(i, side);
|
|
519
|
+
|
|
520
|
+
entries.push({
|
|
521
|
+
text: line + (i < lines.length - 1 ? "\n" : ""),
|
|
522
|
+
properties: {
|
|
523
|
+
type: inConflict ? "conflict-line" : "normal-line",
|
|
524
|
+
lineNumber: i + 1,
|
|
525
|
+
side: side,
|
|
526
|
+
...(inConflict ? { conflictIndex: getConflictIndexForLine(i, side) } : {}),
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return entries;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Check if a line number falls within a conflict region
|
|
536
|
+
*/
|
|
537
|
+
function isLineInConflict(_lineNumber: number, _side: "ours" | "theirs"): boolean {
|
|
538
|
+
// For now, we don't have line mapping from git versions to original file
|
|
539
|
+
// This would require proper diff/alignment between versions
|
|
540
|
+
// TODO: Implement proper line-to-conflict mapping
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Get the conflict index for a line number
|
|
546
|
+
*/
|
|
547
|
+
function getConflictIndexForLine(_lineNumber: number, _side: "ours" | "theirs"): number {
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Build entries showing the merged result content
|
|
553
|
+
* This shows the file with resolved/unresolved conflict regions
|
|
554
|
+
*/
|
|
555
|
+
function buildResultFileEntries(): TextPropertyEntry[] {
|
|
556
|
+
const entries: TextPropertyEntry[] = [];
|
|
557
|
+
|
|
558
|
+
// Build the result by combining non-conflict regions with resolved conflicts
|
|
559
|
+
const originalContent = mergeState.originalContent;
|
|
560
|
+
if (!originalContent) {
|
|
561
|
+
entries.push({
|
|
562
|
+
text: "(No content available)\n",
|
|
563
|
+
properties: { type: "error" },
|
|
564
|
+
});
|
|
565
|
+
return entries;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Parse the original content and replace conflict regions with resolutions
|
|
569
|
+
let result = originalContent;
|
|
570
|
+
|
|
571
|
+
// Process conflicts in reverse order to maintain correct positions
|
|
572
|
+
const sortedConflicts = [...mergeState.conflicts].sort((a, b) => b.startOffset - a.startOffset);
|
|
573
|
+
|
|
574
|
+
for (const conflict of sortedConflicts) {
|
|
575
|
+
let replacement: string;
|
|
576
|
+
|
|
577
|
+
if (conflict.resolved && conflict.resolvedContent !== undefined) {
|
|
578
|
+
replacement = conflict.resolvedContent;
|
|
579
|
+
} else {
|
|
580
|
+
// Show unresolved conflict with markers
|
|
581
|
+
replacement = `<<<<<<< OURS\n${conflict.ours || ""}\n=======\n${conflict.theirs || ""}\n>>>>>>> THEIRS`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Replace the conflict region in the result
|
|
585
|
+
const before = result.substring(0, conflict.startOffset);
|
|
586
|
+
const after = result.substring(conflict.endOffset);
|
|
587
|
+
result = before + replacement + after;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Now display the result content
|
|
591
|
+
const lines = result.split("\n");
|
|
592
|
+
for (let i = 0; i < lines.length; i++) {
|
|
593
|
+
const line = lines[i];
|
|
594
|
+
const isConflictMarker = line.startsWith("<<<<<<<") || line.startsWith("=======") || line.startsWith(">>>>>>>");
|
|
595
|
+
|
|
596
|
+
entries.push({
|
|
597
|
+
text: line + (i < lines.length - 1 ? "\n" : ""),
|
|
598
|
+
properties: {
|
|
599
|
+
type: isConflictMarker ? "conflict-marker" : "result-line",
|
|
600
|
+
lineNumber: i + 1,
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return entries;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// =============================================================================
|
|
609
|
+
// View Rendering - Summary Style (Legacy)
|
|
610
|
+
// =============================================================================
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Build entries for OURS panel (summary style)
|
|
614
|
+
*/
|
|
615
|
+
function buildOursEntries(): TextPropertyEntry[] {
|
|
616
|
+
const entries: TextPropertyEntry[] = [];
|
|
617
|
+
|
|
618
|
+
// Header
|
|
619
|
+
entries.push({
|
|
620
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
621
|
+
properties: { type: "separator" },
|
|
622
|
+
});
|
|
623
|
+
entries.push({
|
|
624
|
+
text: " " + editor.t("panel.ours_header") + "\n",
|
|
625
|
+
properties: { type: "header", panel: "ours" },
|
|
626
|
+
});
|
|
627
|
+
entries.push({
|
|
628
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
629
|
+
properties: { type: "separator" },
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Show each conflict's OURS side
|
|
633
|
+
for (const conflict of mergeState.conflicts) {
|
|
634
|
+
const isSelected = conflict.index === mergeState.selectedIndex;
|
|
635
|
+
const marker = isSelected ? "> " : " ";
|
|
636
|
+
const status = conflict.resolved ? editor.t("panel.resolved") : editor.t("panel.pending");
|
|
637
|
+
|
|
638
|
+
entries.push({
|
|
639
|
+
text: `\n${marker}${editor.t("panel.conflict", { index: String(conflict.index + 1) })} ${status}\n`,
|
|
640
|
+
properties: {
|
|
641
|
+
type: "conflict-header",
|
|
642
|
+
conflictIndex: conflict.index,
|
|
643
|
+
selected: isSelected,
|
|
644
|
+
resolved: conflict.resolved,
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
entries.push({
|
|
649
|
+
text: "─────────────────────────────────────────────────────────────────────────────\n",
|
|
650
|
+
properties: { type: "separator" },
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Content
|
|
654
|
+
const content = conflict.ours || editor.t("panel.empty");
|
|
655
|
+
for (const line of content.split("\n")) {
|
|
656
|
+
entries.push({
|
|
657
|
+
text: ` ${line}\n`,
|
|
658
|
+
properties: {
|
|
659
|
+
type: "conflict-content",
|
|
660
|
+
conflictIndex: conflict.index,
|
|
661
|
+
side: "ours",
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return entries;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Build entries for THEIRS panel
|
|
672
|
+
*/
|
|
673
|
+
function buildTheirsEntries(): TextPropertyEntry[] {
|
|
674
|
+
const entries: TextPropertyEntry[] = [];
|
|
675
|
+
|
|
676
|
+
// Header
|
|
677
|
+
entries.push({
|
|
678
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
679
|
+
properties: { type: "separator" },
|
|
680
|
+
});
|
|
681
|
+
entries.push({
|
|
682
|
+
text: " " + editor.t("panel.theirs_header") + "\n",
|
|
683
|
+
properties: { type: "header", panel: "theirs" },
|
|
684
|
+
});
|
|
685
|
+
entries.push({
|
|
686
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
687
|
+
properties: { type: "separator" },
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Show each conflict's THEIRS side
|
|
691
|
+
for (const conflict of mergeState.conflicts) {
|
|
692
|
+
const isSelected = conflict.index === mergeState.selectedIndex;
|
|
693
|
+
const marker = isSelected ? "> " : " ";
|
|
694
|
+
const status = conflict.resolved ? editor.t("panel.resolved") : editor.t("panel.pending");
|
|
695
|
+
|
|
696
|
+
entries.push({
|
|
697
|
+
text: `\n${marker}${editor.t("panel.conflict", { index: String(conflict.index + 1) })} ${status}\n`,
|
|
698
|
+
properties: {
|
|
699
|
+
type: "conflict-header",
|
|
700
|
+
conflictIndex: conflict.index,
|
|
701
|
+
selected: isSelected,
|
|
702
|
+
resolved: conflict.resolved,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
entries.push({
|
|
707
|
+
text: "─────────────────────────────────────────────────────────────────────────────\n",
|
|
708
|
+
properties: { type: "separator" },
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Content
|
|
712
|
+
const content = conflict.theirs || editor.t("panel.empty");
|
|
713
|
+
for (const line of content.split("\n")) {
|
|
714
|
+
entries.push({
|
|
715
|
+
text: ` ${line}\n`,
|
|
716
|
+
properties: {
|
|
717
|
+
type: "conflict-content",
|
|
718
|
+
conflictIndex: conflict.index,
|
|
719
|
+
side: "theirs",
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return entries;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Build entries for RESULT panel
|
|
730
|
+
*/
|
|
731
|
+
function buildResultEntries(): TextPropertyEntry[] {
|
|
732
|
+
const entries: TextPropertyEntry[] = [];
|
|
733
|
+
|
|
734
|
+
// Header
|
|
735
|
+
entries.push({
|
|
736
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
737
|
+
properties: { type: "separator" },
|
|
738
|
+
});
|
|
739
|
+
entries.push({
|
|
740
|
+
text: " " + editor.t("panel.result_header") + "\n",
|
|
741
|
+
properties: { type: "header", panel: "result" },
|
|
742
|
+
});
|
|
743
|
+
entries.push({
|
|
744
|
+
text: "═══════════════════════════════════════════════════════════════════════════════\n",
|
|
745
|
+
properties: { type: "separator" },
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Build result content
|
|
749
|
+
const unresolvedCount = mergeState.conflicts.filter(c => !c.resolved).length;
|
|
750
|
+
|
|
751
|
+
if (unresolvedCount > 0) {
|
|
752
|
+
entries.push({
|
|
753
|
+
text: `\n ⚠ ${editor.t("panel.remaining", { count: String(unresolvedCount) })}\n\n`,
|
|
754
|
+
properties: { type: "warning" },
|
|
755
|
+
});
|
|
756
|
+
} else {
|
|
757
|
+
entries.push({
|
|
758
|
+
text: "\n ✓ " + editor.t("panel.all_resolved") + "\n\n",
|
|
759
|
+
properties: { type: "success" },
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Show resolved content or action buttons for each conflict
|
|
764
|
+
for (const conflict of mergeState.conflicts) {
|
|
765
|
+
const isSelected = conflict.index === mergeState.selectedIndex;
|
|
766
|
+
const marker = isSelected ? "> " : " ";
|
|
767
|
+
|
|
768
|
+
entries.push({
|
|
769
|
+
text: `${marker}${editor.t("panel.conflict", { index: String(conflict.index + 1) })}:\n`,
|
|
770
|
+
properties: {
|
|
771
|
+
type: "conflict-header",
|
|
772
|
+
conflictIndex: conflict.index,
|
|
773
|
+
selected: isSelected,
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (conflict.resolved && conflict.resolvedContent !== undefined) {
|
|
778
|
+
// Show resolved content
|
|
779
|
+
entries.push({
|
|
780
|
+
text: ` ${editor.t("panel.resolved_with", { resolution: conflict.resolution || "" })}\n`,
|
|
781
|
+
properties: { type: "resolution-info", resolution: conflict.resolution },
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
for (const line of conflict.resolvedContent.split("\n")) {
|
|
785
|
+
entries.push({
|
|
786
|
+
text: ` ${line}\n`,
|
|
787
|
+
properties: {
|
|
788
|
+
type: "resolved-content",
|
|
789
|
+
conflictIndex: conflict.index,
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
// Show clickable action buttons
|
|
795
|
+
// Each button is a separate entry with onClick for mouse support
|
|
796
|
+
entries.push({
|
|
797
|
+
text: " << ",
|
|
798
|
+
properties: { type: "action-prefix" },
|
|
799
|
+
});
|
|
800
|
+
entries.push({
|
|
801
|
+
text: editor.t("btn.accept_ours"),
|
|
802
|
+
properties: {
|
|
803
|
+
type: "action-button",
|
|
804
|
+
conflictIndex: conflict.index,
|
|
805
|
+
onClick: "merge_use_ours",
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
entries.push({
|
|
809
|
+
text: " | ",
|
|
810
|
+
properties: { type: "action-separator" },
|
|
811
|
+
});
|
|
812
|
+
entries.push({
|
|
813
|
+
text: editor.t("btn.accept_theirs"),
|
|
814
|
+
properties: {
|
|
815
|
+
type: "action-button",
|
|
816
|
+
conflictIndex: conflict.index,
|
|
817
|
+
onClick: "merge_take_theirs",
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
entries.push({
|
|
821
|
+
text: " | ",
|
|
822
|
+
properties: { type: "action-separator" },
|
|
823
|
+
});
|
|
824
|
+
entries.push({
|
|
825
|
+
text: editor.t("btn.both"),
|
|
826
|
+
properties: {
|
|
827
|
+
type: "action-button",
|
|
828
|
+
conflictIndex: conflict.index,
|
|
829
|
+
onClick: "merge_use_both",
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
entries.push({
|
|
833
|
+
text: " >>\n",
|
|
834
|
+
properties: { type: "action-suffix" },
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
entries.push({
|
|
839
|
+
text: "─────────────────────────────────────────────────────────────────────────────\n",
|
|
840
|
+
properties: { type: "separator" },
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Help bar with clickable buttons
|
|
845
|
+
entries.push({
|
|
846
|
+
text: "\n",
|
|
847
|
+
properties: { type: "blank" },
|
|
848
|
+
});
|
|
849
|
+
// Navigation
|
|
850
|
+
entries.push({
|
|
851
|
+
text: editor.t("btn.next"),
|
|
852
|
+
properties: { type: "help-button", onClick: "merge_next_conflict" },
|
|
853
|
+
});
|
|
854
|
+
entries.push({
|
|
855
|
+
text: " ",
|
|
856
|
+
properties: { type: "help-separator" },
|
|
857
|
+
});
|
|
858
|
+
entries.push({
|
|
859
|
+
text: editor.t("btn.prev"),
|
|
860
|
+
properties: { type: "help-button", onClick: "merge_prev_conflict" },
|
|
861
|
+
});
|
|
862
|
+
entries.push({
|
|
863
|
+
text: " | ",
|
|
864
|
+
properties: { type: "help-separator" },
|
|
865
|
+
});
|
|
866
|
+
// Resolution
|
|
867
|
+
entries.push({
|
|
868
|
+
text: editor.t("btn.use_ours"),
|
|
869
|
+
properties: { type: "help-button", onClick: "merge_use_ours" },
|
|
870
|
+
});
|
|
871
|
+
entries.push({
|
|
872
|
+
text: " ",
|
|
873
|
+
properties: { type: "help-separator" },
|
|
874
|
+
});
|
|
875
|
+
entries.push({
|
|
876
|
+
text: editor.t("btn.take_theirs"),
|
|
877
|
+
properties: { type: "help-button", onClick: "merge_take_theirs" },
|
|
878
|
+
});
|
|
879
|
+
entries.push({
|
|
880
|
+
text: " ",
|
|
881
|
+
properties: { type: "help-separator" },
|
|
882
|
+
});
|
|
883
|
+
entries.push({
|
|
884
|
+
text: editor.t("btn.both"),
|
|
885
|
+
properties: { type: "help-button", onClick: "merge_use_both" },
|
|
886
|
+
});
|
|
887
|
+
entries.push({
|
|
888
|
+
text: " | ",
|
|
889
|
+
properties: { type: "help-separator" },
|
|
890
|
+
});
|
|
891
|
+
// Completion
|
|
892
|
+
entries.push({
|
|
893
|
+
text: editor.t("btn.save_exit"),
|
|
894
|
+
properties: { type: "help-button", onClick: "merge_save_and_exit" },
|
|
895
|
+
});
|
|
896
|
+
entries.push({
|
|
897
|
+
text: " ",
|
|
898
|
+
properties: { type: "help-separator" },
|
|
899
|
+
});
|
|
900
|
+
entries.push({
|
|
901
|
+
text: editor.t("btn.abort"),
|
|
902
|
+
properties: { type: "help-button", onClick: "merge_abort" },
|
|
903
|
+
});
|
|
904
|
+
entries.push({
|
|
905
|
+
text: "\n",
|
|
906
|
+
properties: { type: "help-newline" },
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
return entries;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Apply visual highlighting to panels
|
|
914
|
+
*/
|
|
915
|
+
function applyHighlighting(): void {
|
|
916
|
+
// Highlight OURS panel
|
|
917
|
+
if (mergeState.oursPanelId !== null) {
|
|
918
|
+
editor.removeOverlaysByPrefix(mergeState.oursPanelId, "merge-");
|
|
919
|
+
highlightPanel(mergeState.oursPanelId, "ours");
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Highlight THEIRS panel
|
|
923
|
+
if (mergeState.theirsPanelId !== null) {
|
|
924
|
+
editor.removeOverlaysByPrefix(mergeState.theirsPanelId, "merge-");
|
|
925
|
+
highlightPanel(mergeState.theirsPanelId, "theirs");
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Highlight RESULT panel
|
|
929
|
+
if (mergeState.resultPanelId !== null) {
|
|
930
|
+
editor.removeOverlaysByPrefix(mergeState.resultPanelId, "merge-");
|
|
931
|
+
highlightResultPanel(mergeState.resultPanelId);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Highlight a side panel (OURS or THEIRS)
|
|
937
|
+
* Note: We compute content from our entries since getBufferText was removed
|
|
938
|
+
*
|
|
939
|
+
* TODO: Implement proper conflict region highlighting:
|
|
940
|
+
* - Find actual conflict regions in git content by searching for conflict.ours/conflict.theirs text
|
|
941
|
+
* - Highlight each conflict region with appropriate color (conflictOurs/conflictTheirs)
|
|
942
|
+
* - Use different highlight for selected conflict vs unselected
|
|
943
|
+
* - Consider using line-based highlighting for better visual effect
|
|
944
|
+
*/
|
|
945
|
+
function highlightPanel(bufferId: number, side: "ours" | "theirs"): void {
|
|
946
|
+
// Build content from entries (same as what we set on the buffer)
|
|
947
|
+
const entries = buildFullFileEntries(side);
|
|
948
|
+
const content = entries.map(e => e.text).join("");
|
|
949
|
+
const lines = content.split("\n");
|
|
950
|
+
|
|
951
|
+
let byteOffset = 0;
|
|
952
|
+
const conflictColor = side === "ours" ? colors.conflictOurs : colors.conflictTheirs;
|
|
953
|
+
|
|
954
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
955
|
+
const line = lines[lineIdx];
|
|
956
|
+
const lineStart = byteOffset;
|
|
957
|
+
const lineEnd = byteOffset + line.length;
|
|
958
|
+
|
|
959
|
+
// Highlight conflict header lines
|
|
960
|
+
if (line.includes("--- Conflict")) {
|
|
961
|
+
editor.addOverlay(
|
|
962
|
+
bufferId,
|
|
963
|
+
`merge-conflict-header-${lineIdx}`,
|
|
964
|
+
lineStart,
|
|
965
|
+
lineEnd,
|
|
966
|
+
conflictColor[0],
|
|
967
|
+
conflictColor[1],
|
|
968
|
+
conflictColor[2],
|
|
969
|
+
true // underline
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
byteOffset = lineEnd + 1;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Highlight the RESULT panel
|
|
979
|
+
* Note: We compute content from our entries since getBufferText was removed
|
|
980
|
+
*/
|
|
981
|
+
function highlightResultPanel(bufferId: number): void {
|
|
982
|
+
// Build content from entries (same as what we set on the buffer)
|
|
983
|
+
const entries = buildResultFileEntries();
|
|
984
|
+
const content = entries.map(e => e.text).join("");
|
|
985
|
+
const lines = content.split("\n");
|
|
986
|
+
|
|
987
|
+
let byteOffset = 0;
|
|
988
|
+
|
|
989
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
990
|
+
const line = lines[lineIdx];
|
|
991
|
+
const lineStart = byteOffset;
|
|
992
|
+
const lineEnd = byteOffset + line.length;
|
|
993
|
+
|
|
994
|
+
// Highlight conflict markers
|
|
995
|
+
if (line.startsWith("<<<<<<<") || line.startsWith("=======") || line.startsWith(">>>>>>>")) {
|
|
996
|
+
editor.addOverlay(
|
|
997
|
+
bufferId,
|
|
998
|
+
`merge-marker-${lineIdx}`,
|
|
999
|
+
lineStart,
|
|
1000
|
+
lineEnd,
|
|
1001
|
+
colors.unresolved[0],
|
|
1002
|
+
colors.unresolved[1],
|
|
1003
|
+
colors.unresolved[2],
|
|
1004
|
+
true // underline
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
byteOffset = lineEnd + 1;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Update all panel views
|
|
1014
|
+
*/
|
|
1015
|
+
function updateViews(): void {
|
|
1016
|
+
if (mergeState.oursPanelId !== null) {
|
|
1017
|
+
editor.setVirtualBufferContent(mergeState.oursPanelId, buildFullFileEntries("ours"));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (mergeState.theirsPanelId !== null) {
|
|
1021
|
+
editor.setVirtualBufferContent(mergeState.theirsPanelId, buildFullFileEntries("theirs"));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (mergeState.resultPanelId !== null) {
|
|
1025
|
+
editor.setVirtualBufferContent(mergeState.resultPanelId, buildResultFileEntries());
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
applyHighlighting();
|
|
1029
|
+
updateStatusBar();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Update status bar with merge progress
|
|
1034
|
+
*/
|
|
1035
|
+
function updateStatusBar(): void {
|
|
1036
|
+
const total = mergeState.conflicts.length;
|
|
1037
|
+
const resolved = mergeState.conflicts.filter(c => c.resolved).length;
|
|
1038
|
+
const remaining = total - resolved;
|
|
1039
|
+
|
|
1040
|
+
if (remaining > 0) {
|
|
1041
|
+
editor.setStatus(editor.t("status.progress", { remaining: String(remaining), total: String(total), current: String(mergeState.selectedIndex + 1) }));
|
|
1042
|
+
} else {
|
|
1043
|
+
editor.setStatus(editor.t("status.all_resolved", { total: String(total) }));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Scroll all three panels to show the selected conflict
|
|
1049
|
+
* This computes the byte offset where the conflict appears in each panel's content
|
|
1050
|
+
* and uses setBufferCursor to scroll the viewport.
|
|
1051
|
+
*/
|
|
1052
|
+
function scrollToSelectedConflict(): void {
|
|
1053
|
+
const conflict = mergeState.conflicts[mergeState.selectedIndex];
|
|
1054
|
+
if (!conflict) return;
|
|
1055
|
+
|
|
1056
|
+
// Scroll OURS panel
|
|
1057
|
+
if (mergeState.oursPanelId !== null) {
|
|
1058
|
+
const oursOffset = computeConflictOffset("ours", conflict.index);
|
|
1059
|
+
if (oursOffset >= 0) {
|
|
1060
|
+
editor.setBufferCursor(mergeState.oursPanelId, oursOffset);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Scroll THEIRS panel
|
|
1065
|
+
if (mergeState.theirsPanelId !== null) {
|
|
1066
|
+
const theirsOffset = computeConflictOffset("theirs", conflict.index);
|
|
1067
|
+
if (theirsOffset >= 0) {
|
|
1068
|
+
editor.setBufferCursor(mergeState.theirsPanelId, theirsOffset);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Scroll RESULT panel
|
|
1073
|
+
if (mergeState.resultPanelId !== null) {
|
|
1074
|
+
const resultOffset = computeResultConflictOffset(conflict.index);
|
|
1075
|
+
if (resultOffset >= 0) {
|
|
1076
|
+
editor.setBufferCursor(mergeState.resultPanelId, resultOffset);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Compute the byte offset where a conflict appears in the OURS or THEIRS panel content.
|
|
1083
|
+
* We search for the actual conflict text (conflict.ours or conflict.theirs) in the
|
|
1084
|
+
* git content to find the exact position.
|
|
1085
|
+
*/
|
|
1086
|
+
function computeConflictOffset(side: "ours" | "theirs", conflictIndex: number): number {
|
|
1087
|
+
const gitContent = side === "ours" ? mergeState.oursContent : mergeState.theirsContent;
|
|
1088
|
+
const conflict = mergeState.conflicts[conflictIndex];
|
|
1089
|
+
|
|
1090
|
+
if (!conflict) return 0;
|
|
1091
|
+
|
|
1092
|
+
// Get the conflict text for this side
|
|
1093
|
+
const conflictText = side === "ours" ? conflict.ours : conflict.theirs;
|
|
1094
|
+
|
|
1095
|
+
if (gitContent && conflictText) {
|
|
1096
|
+
// Strategy 1: Search for the exact conflict text (trimmed)
|
|
1097
|
+
const trimmedText = conflictText.trim();
|
|
1098
|
+
if (trimmedText.length > 0) {
|
|
1099
|
+
const pos = gitContent.indexOf(trimmedText);
|
|
1100
|
+
if (pos >= 0) {
|
|
1101
|
+
return pos;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Strategy 2: Search for the first line of the conflict
|
|
1106
|
+
const firstLine = conflictText.split("\n")[0]?.trim();
|
|
1107
|
+
if (firstLine && firstLine.length > 5) {
|
|
1108
|
+
const pos = gitContent.indexOf(firstLine);
|
|
1109
|
+
if (pos >= 0) {
|
|
1110
|
+
return pos;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Strategy 3: Ratio-based fallback
|
|
1115
|
+
const originalLength = mergeState.originalContent.length;
|
|
1116
|
+
if (originalLength > 0) {
|
|
1117
|
+
const ratio = conflict.startOffset / originalLength;
|
|
1118
|
+
return Math.floor(ratio * gitContent.length);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// If no git content, we built entries manually - find "--- Conflict N ---"
|
|
1123
|
+
const entries = buildFullFileEntries(side);
|
|
1124
|
+
let offset = 0;
|
|
1125
|
+
for (const entry of entries) {
|
|
1126
|
+
if (entry.text.includes(`--- Conflict ${conflictIndex + 1} ---`)) {
|
|
1127
|
+
return offset;
|
|
1128
|
+
}
|
|
1129
|
+
offset += entry.text.length;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return 0;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Compute the byte offset where a conflict appears in the RESULT panel content.
|
|
1137
|
+
* The RESULT panel shows the original file with conflict markers (<<<<<<< OURS, etc.)
|
|
1138
|
+
* We need to find the Nth <<<<<<< marker.
|
|
1139
|
+
*/
|
|
1140
|
+
function computeResultConflictOffset(conflictIndex: number): number {
|
|
1141
|
+
const entries = buildResultFileEntries();
|
|
1142
|
+
const content = entries.map(e => e.text).join("");
|
|
1143
|
+
|
|
1144
|
+
// Find the Nth occurrence of <<<<<<< marker
|
|
1145
|
+
let searchPos = 0;
|
|
1146
|
+
let conflictCount = 0;
|
|
1147
|
+
|
|
1148
|
+
while (searchPos < content.length) {
|
|
1149
|
+
const markerPos = content.indexOf("<<<<<<<", searchPos);
|
|
1150
|
+
if (markerPos === -1) break;
|
|
1151
|
+
|
|
1152
|
+
if (conflictCount === conflictIndex) {
|
|
1153
|
+
return markerPos;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
conflictCount++;
|
|
1157
|
+
searchPos = markerPos + 7; // Skip past "<<<<<<<" to continue searching
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Fallback: use ratio-based estimation like we do for OURS/THEIRS
|
|
1161
|
+
const conflict = mergeState.conflicts[conflictIndex];
|
|
1162
|
+
if (conflict && mergeState.originalContent.length > 0) {
|
|
1163
|
+
const ratio = conflict.startOffset / mergeState.originalContent.length;
|
|
1164
|
+
return Math.floor(ratio * content.length);
|
|
1165
|
+
}
|
|
1166
|
+
return 0;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// =============================================================================
|
|
1170
|
+
// Public Commands - Activation
|
|
1171
|
+
// =============================================================================
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Start merge conflict resolution for current buffer
|
|
1175
|
+
*/
|
|
1176
|
+
globalThis.start_merge_conflict = async function(): Promise<void> {
|
|
1177
|
+
if (mergeState.isActive) {
|
|
1178
|
+
editor.setStatus(editor.t("status.already_active"));
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const bufferId = editor.getActiveBufferId();
|
|
1183
|
+
const info = editor.getBufferInfo(bufferId);
|
|
1184
|
+
|
|
1185
|
+
if (!info || !info.path) {
|
|
1186
|
+
editor.setStatus(editor.t("status.no_file"));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
editor.debug(`Merge: starting for ${info.path}`);
|
|
1191
|
+
|
|
1192
|
+
// Get the directory of the file for running git commands
|
|
1193
|
+
const fileDir = editor.pathDirname(info.path);
|
|
1194
|
+
editor.debug(`Merge: file directory is ${fileDir}`);
|
|
1195
|
+
|
|
1196
|
+
// Check if we're in a git repo (run from file's directory)
|
|
1197
|
+
const gitCheck = await editor.spawnProcess("git", ["rev-parse", "--is-inside-work-tree"], fileDir);
|
|
1198
|
+
editor.debug(`Merge: git rev-parse exit_code=${gitCheck.exit_code}, stdout=${gitCheck.stdout.trim()}`);
|
|
1199
|
+
|
|
1200
|
+
if (gitCheck.exit_code !== 0 || gitCheck.stdout.trim() !== "true") {
|
|
1201
|
+
editor.setStatus(editor.t("status.not_git_repo"));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Check if file has unmerged entries using git (run from file's directory)
|
|
1206
|
+
const lsFilesResult = await editor.spawnProcess("git", ["ls-files", "-u", info.path], fileDir);
|
|
1207
|
+
editor.debug(`Merge: git ls-files -u exit_code=${lsFilesResult.exit_code}, stdout length=${lsFilesResult.stdout.length}, stderr=${lsFilesResult.stderr}`);
|
|
1208
|
+
|
|
1209
|
+
const hasUnmergedEntries = lsFilesResult.exit_code === 0 && lsFilesResult.stdout.trim().length > 0;
|
|
1210
|
+
|
|
1211
|
+
if (!hasUnmergedEntries) {
|
|
1212
|
+
editor.setStatus(editor.t("status.no_unmerged"));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Get file content from git's working tree (has conflict markers)
|
|
1217
|
+
const catFileResult = await editor.spawnProcess("git", ["show", `:0:${info.path}`]);
|
|
1218
|
+
|
|
1219
|
+
// If :0: doesn't exist, read the working tree file directly
|
|
1220
|
+
let content: string;
|
|
1221
|
+
if (catFileResult.exit_code !== 0) {
|
|
1222
|
+
editor.debug(`Merge: git show :0: failed, reading working tree file`);
|
|
1223
|
+
const fileContent = await editor.readFile(info.path);
|
|
1224
|
+
if (!fileContent) {
|
|
1225
|
+
editor.setStatus(editor.t("status.failed_read"));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
content = fileContent;
|
|
1229
|
+
} else {
|
|
1230
|
+
// The staged version shouldn't have conflict markers, use working tree
|
|
1231
|
+
const fileContent = await editor.readFile(info.path);
|
|
1232
|
+
if (!fileContent) {
|
|
1233
|
+
editor.setStatus(editor.t("status.failed_read"));
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
content = fileContent;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Check for conflict markers in content
|
|
1240
|
+
const hasMarkers = hasConflictMarkers(content);
|
|
1241
|
+
editor.debug(`Merge: file has conflict markers: ${hasMarkers}, content length: ${content.length}`);
|
|
1242
|
+
|
|
1243
|
+
if (!hasMarkers) {
|
|
1244
|
+
editor.setStatus(editor.t("status.no_markers"));
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
editor.setStatus(editor.t("status.starting"));
|
|
1249
|
+
|
|
1250
|
+
// Store original state
|
|
1251
|
+
mergeState.sourceBufferId = bufferId;
|
|
1252
|
+
mergeState.sourcePath = info.path;
|
|
1253
|
+
mergeState.originalContent = content;
|
|
1254
|
+
|
|
1255
|
+
// Parse conflicts
|
|
1256
|
+
mergeState.conflicts = parseConflicts(content);
|
|
1257
|
+
|
|
1258
|
+
// Debug: log parse results
|
|
1259
|
+
editor.debug(`Merge: parseConflicts found ${mergeState.conflicts.length} conflicts`);
|
|
1260
|
+
|
|
1261
|
+
if (mergeState.conflicts.length === 0) {
|
|
1262
|
+
editor.setStatus(editor.t("status.failed_parse"));
|
|
1263
|
+
// Log more detail for debugging
|
|
1264
|
+
editor.debug(`Merge: regex failed, content has <<<<<<< at index ${content.indexOf("<<<<<<<")}`);
|
|
1265
|
+
editor.debug(`Merge: content around <<<<<<< : ${content.substring(content.indexOf("<<<<<<<") - 20, content.indexOf("<<<<<<<") + 100)}`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
editor.debug(`Found ${mergeState.conflicts.length} conflicts`);
|
|
1270
|
+
|
|
1271
|
+
// Fetch git versions for auto-resolution
|
|
1272
|
+
const versions = await fetchGitVersions(info.path);
|
|
1273
|
+
if (versions) {
|
|
1274
|
+
mergeState.baseContent = versions.base;
|
|
1275
|
+
mergeState.oursContent = versions.ours;
|
|
1276
|
+
mergeState.theirsContent = versions.theirs;
|
|
1277
|
+
editor.debug("Fetched git versions for auto-resolution");
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Attempt auto-resolution
|
|
1281
|
+
autoResolveConflicts(mergeState.conflicts);
|
|
1282
|
+
|
|
1283
|
+
const autoResolved = mergeState.conflicts.filter(c => c.resolved).length;
|
|
1284
|
+
if (autoResolved > 0) {
|
|
1285
|
+
editor.debug(`Auto-resolved ${autoResolved} trivial conflicts`);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Find first unresolved conflict
|
|
1289
|
+
mergeState.selectedIndex = 0;
|
|
1290
|
+
for (let i = 0; i < mergeState.conflicts.length; i++) {
|
|
1291
|
+
if (!mergeState.conflicts[i].resolved) {
|
|
1292
|
+
mergeState.selectedIndex = i;
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Create the merge UI panels
|
|
1298
|
+
await createMergePanels();
|
|
1299
|
+
|
|
1300
|
+
mergeState.isActive = true;
|
|
1301
|
+
|
|
1302
|
+
// Register merge-mode commands now that we're active
|
|
1303
|
+
registerMergeModeCommands();
|
|
1304
|
+
|
|
1305
|
+
updateViews();
|
|
1306
|
+
|
|
1307
|
+
// Scroll all panels to show the first conflict
|
|
1308
|
+
scrollToSelectedConflict();
|
|
1309
|
+
|
|
1310
|
+
const remaining = mergeState.conflicts.length - autoResolved;
|
|
1311
|
+
if (remaining > 0) {
|
|
1312
|
+
editor.setStatus(editor.t("status.conflicts_to_resolve", { remaining: String(remaining), auto_resolved: String(autoResolved) }));
|
|
1313
|
+
} else {
|
|
1314
|
+
editor.setStatus(editor.t("status.all_auto_resolved", { total: String(mergeState.conflicts.length) }));
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Create the multi-panel merge UI (JetBrains-style: OURS | RESULT | THEIRS)
|
|
1320
|
+
*
|
|
1321
|
+
* Creates three vertical splits and then calls distributeSplitsEvenly()
|
|
1322
|
+
* to ensure all panels get equal width.
|
|
1323
|
+
*/
|
|
1324
|
+
async function createMergePanels(): Promise<void> {
|
|
1325
|
+
// Get the source file's extension for syntax highlighting
|
|
1326
|
+
// Tree-sitter uses filename extension to determine language
|
|
1327
|
+
const sourceExt = mergeState.sourcePath
|
|
1328
|
+
? mergeState.sourcePath.substring(mergeState.sourcePath.lastIndexOf("."))
|
|
1329
|
+
: "";
|
|
1330
|
+
|
|
1331
|
+
editor.debug(`Merge: source extension '${sourceExt}' for syntax highlighting`);
|
|
1332
|
+
|
|
1333
|
+
// Create OURS panel first (takes over current view)
|
|
1334
|
+
// Include extension in name so tree-sitter can apply highlighting
|
|
1335
|
+
const oursId = await editor.createVirtualBuffer({
|
|
1336
|
+
name: `*OURS*${sourceExt}`,
|
|
1337
|
+
mode: "merge-conflict",
|
|
1338
|
+
read_only: true,
|
|
1339
|
+
entries: buildFullFileEntries("ours"),
|
|
1340
|
+
panel_id: "merge-ours",
|
|
1341
|
+
show_line_numbers: true,
|
|
1342
|
+
show_cursors: true,
|
|
1343
|
+
editing_disabled: true,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
if (oursId !== null) {
|
|
1347
|
+
mergeState.oursPanelId = oursId;
|
|
1348
|
+
mergeState.oursSplitId = editor.getActiveSplitId();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Create THEIRS panel to the right (vertical split)
|
|
1352
|
+
const theirsId = await editor.createVirtualBufferInSplit({
|
|
1353
|
+
name: `*THEIRS*${sourceExt}`,
|
|
1354
|
+
mode: "merge-conflict",
|
|
1355
|
+
read_only: true,
|
|
1356
|
+
entries: buildFullFileEntries("theirs"),
|
|
1357
|
+
ratio: 0.5, // Will be equalized by distributeSplitsEvenly
|
|
1358
|
+
direction: "vertical",
|
|
1359
|
+
panel_id: "merge-theirs",
|
|
1360
|
+
show_line_numbers: true,
|
|
1361
|
+
show_cursors: true,
|
|
1362
|
+
editing_disabled: true,
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
if (theirsId !== null) {
|
|
1366
|
+
mergeState.theirsPanelId = theirsId;
|
|
1367
|
+
mergeState.theirsSplitId = editor.getActiveSplitId();
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Focus back on OURS and create RESULT in the middle
|
|
1371
|
+
if (mergeState.oursSplitId !== null) {
|
|
1372
|
+
editor.focusSplit(mergeState.oursSplitId);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const resultId = await editor.createVirtualBufferInSplit({
|
|
1376
|
+
name: `*RESULT*${sourceExt}`,
|
|
1377
|
+
mode: "merge-result",
|
|
1378
|
+
read_only: false,
|
|
1379
|
+
entries: buildResultFileEntries(),
|
|
1380
|
+
ratio: 0.5, // Will be equalized by distributeSplitsEvenly
|
|
1381
|
+
direction: "vertical",
|
|
1382
|
+
panel_id: "merge-result",
|
|
1383
|
+
show_line_numbers: true,
|
|
1384
|
+
show_cursors: true,
|
|
1385
|
+
editing_disabled: false,
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
if (resultId !== null) {
|
|
1389
|
+
mergeState.resultPanelId = resultId;
|
|
1390
|
+
mergeState.resultSplitId = editor.getActiveSplitId();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Distribute splits evenly so all three panels get equal width
|
|
1394
|
+
editor.distributeSplitsEvenly();
|
|
1395
|
+
|
|
1396
|
+
// Focus the RESULT panel since that's where the user will resolve conflicts
|
|
1397
|
+
if (mergeState.resultSplitId !== null) {
|
|
1398
|
+
editor.focusSplit(mergeState.resultSplitId);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// =============================================================================
|
|
1403
|
+
// Public Commands - Navigation
|
|
1404
|
+
// =============================================================================
|
|
1405
|
+
|
|
1406
|
+
globalThis.merge_next_conflict = function(): void {
|
|
1407
|
+
editor.debug(`merge_next_conflict called, isActive=${mergeState.isActive}, conflicts=${mergeState.conflicts.length}`);
|
|
1408
|
+
|
|
1409
|
+
if (!mergeState.isActive) {
|
|
1410
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (mergeState.conflicts.length === 0) {
|
|
1414
|
+
editor.setStatus(editor.t("status.no_conflicts"));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (mergeState.conflicts.length === 1) {
|
|
1418
|
+
// Single conflict: just re-scroll to it (useful for re-focusing)
|
|
1419
|
+
editor.setStatus(editor.t("status.single_refocused"));
|
|
1420
|
+
scrollToSelectedConflict();
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Find next unresolved conflict (or wrap around)
|
|
1425
|
+
let startIndex = mergeState.selectedIndex;
|
|
1426
|
+
let index = (startIndex + 1) % mergeState.conflicts.length;
|
|
1427
|
+
|
|
1428
|
+
// First try to find next unresolved
|
|
1429
|
+
while (index !== startIndex) {
|
|
1430
|
+
if (!mergeState.conflicts[index].resolved) {
|
|
1431
|
+
mergeState.selectedIndex = index;
|
|
1432
|
+
editor.setStatus(editor.t("status.conflict_of", { current: String(index + 1), total: String(mergeState.conflicts.length) }));
|
|
1433
|
+
updateViews();
|
|
1434
|
+
scrollToSelectedConflict();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
index = (index + 1) % mergeState.conflicts.length;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// If all resolved, just move to next
|
|
1441
|
+
mergeState.selectedIndex = (mergeState.selectedIndex + 1) % mergeState.conflicts.length;
|
|
1442
|
+
editor.setStatus(editor.t("status.conflict_all_resolved", { current: String(mergeState.selectedIndex + 1), total: String(mergeState.conflicts.length) }));
|
|
1443
|
+
updateViews();
|
|
1444
|
+
scrollToSelectedConflict();
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
globalThis.merge_prev_conflict = function(): void {
|
|
1448
|
+
editor.debug(`merge_prev_conflict called, isActive=${mergeState.isActive}, conflicts=${mergeState.conflicts.length}`);
|
|
1449
|
+
|
|
1450
|
+
if (!mergeState.isActive) {
|
|
1451
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (mergeState.conflicts.length === 0) {
|
|
1455
|
+
editor.setStatus(editor.t("status.no_conflicts"));
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (mergeState.conflicts.length === 1) {
|
|
1459
|
+
// Single conflict: just re-scroll to it (useful for re-focusing)
|
|
1460
|
+
editor.setStatus(editor.t("status.single_refocused"));
|
|
1461
|
+
scrollToSelectedConflict();
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Find previous unresolved conflict (or wrap around)
|
|
1466
|
+
let startIndex = mergeState.selectedIndex;
|
|
1467
|
+
let index = (startIndex - 1 + mergeState.conflicts.length) % mergeState.conflicts.length;
|
|
1468
|
+
|
|
1469
|
+
// First try to find previous unresolved
|
|
1470
|
+
while (index !== startIndex) {
|
|
1471
|
+
if (!mergeState.conflicts[index].resolved) {
|
|
1472
|
+
mergeState.selectedIndex = index;
|
|
1473
|
+
editor.setStatus(editor.t("status.conflict_of", { current: String(index + 1), total: String(mergeState.conflicts.length) }));
|
|
1474
|
+
updateViews();
|
|
1475
|
+
scrollToSelectedConflict();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
index = (index - 1 + mergeState.conflicts.length) % mergeState.conflicts.length;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// If all resolved, just move to previous
|
|
1482
|
+
mergeState.selectedIndex = (mergeState.selectedIndex - 1 + mergeState.conflicts.length) % mergeState.conflicts.length;
|
|
1483
|
+
editor.setStatus(editor.t("status.conflict_all_resolved", { current: String(mergeState.selectedIndex + 1), total: String(mergeState.conflicts.length) }));
|
|
1484
|
+
updateViews();
|
|
1485
|
+
scrollToSelectedConflict();
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
// =============================================================================
|
|
1489
|
+
// Public Commands - Resolution
|
|
1490
|
+
// =============================================================================
|
|
1491
|
+
|
|
1492
|
+
globalThis.merge_use_ours = function(): void {
|
|
1493
|
+
if (!mergeState.isActive) {
|
|
1494
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const conflict = mergeState.conflicts[mergeState.selectedIndex];
|
|
1499
|
+
if (!conflict) return;
|
|
1500
|
+
|
|
1501
|
+
conflict.resolved = true;
|
|
1502
|
+
conflict.resolution = "ours";
|
|
1503
|
+
conflict.resolvedContent = conflict.ours;
|
|
1504
|
+
|
|
1505
|
+
editor.debug(`Resolved conflict ${conflict.index} with OURS`);
|
|
1506
|
+
|
|
1507
|
+
// Move to next unresolved conflict
|
|
1508
|
+
moveToNextUnresolved();
|
|
1509
|
+
updateViews();
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
globalThis.merge_take_theirs = function(): void {
|
|
1513
|
+
if (!mergeState.isActive) {
|
|
1514
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const conflict = mergeState.conflicts[mergeState.selectedIndex];
|
|
1519
|
+
if (!conflict) return;
|
|
1520
|
+
|
|
1521
|
+
conflict.resolved = true;
|
|
1522
|
+
conflict.resolution = "theirs";
|
|
1523
|
+
conflict.resolvedContent = conflict.theirs;
|
|
1524
|
+
|
|
1525
|
+
editor.debug(`Resolved conflict ${conflict.index} with THEIRS`);
|
|
1526
|
+
|
|
1527
|
+
// Move to next unresolved conflict
|
|
1528
|
+
moveToNextUnresolved();
|
|
1529
|
+
updateViews();
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
globalThis.merge_use_both = function(): void {
|
|
1533
|
+
if (!mergeState.isActive) {
|
|
1534
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const conflict = mergeState.conflicts[mergeState.selectedIndex];
|
|
1539
|
+
if (!conflict) return;
|
|
1540
|
+
|
|
1541
|
+
conflict.resolved = true;
|
|
1542
|
+
conflict.resolution = "both";
|
|
1543
|
+
conflict.resolvedContent = conflict.ours + conflict.theirs;
|
|
1544
|
+
|
|
1545
|
+
editor.debug(`Resolved conflict ${conflict.index} with BOTH`);
|
|
1546
|
+
|
|
1547
|
+
// Move to next unresolved conflict
|
|
1548
|
+
moveToNextUnresolved();
|
|
1549
|
+
updateViews();
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Move selection to the next unresolved conflict
|
|
1554
|
+
*/
|
|
1555
|
+
function moveToNextUnresolved(): void {
|
|
1556
|
+
const startIndex = mergeState.selectedIndex;
|
|
1557
|
+
let index = (startIndex + 1) % mergeState.conflicts.length;
|
|
1558
|
+
|
|
1559
|
+
while (index !== startIndex) {
|
|
1560
|
+
if (!mergeState.conflicts[index].resolved) {
|
|
1561
|
+
mergeState.selectedIndex = index;
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
index = (index + 1) % mergeState.conflicts.length;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// All resolved, stay where we are
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// =============================================================================
|
|
1571
|
+
// Public Commands - Completion
|
|
1572
|
+
// =============================================================================
|
|
1573
|
+
|
|
1574
|
+
globalThis.merge_save_and_exit = async function(): Promise<void> {
|
|
1575
|
+
if (!mergeState.isActive) {
|
|
1576
|
+
editor.setStatus(editor.t("status.no_active_merge"));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const unresolvedCount = mergeState.conflicts.filter(c => !c.resolved).length;
|
|
1581
|
+
|
|
1582
|
+
if (unresolvedCount > 0) {
|
|
1583
|
+
// TODO: Add confirmation prompt
|
|
1584
|
+
editor.setStatus(editor.t("status.cannot_save", { count: String(unresolvedCount) }));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Build final content by replacing conflict markers with resolved content
|
|
1589
|
+
let finalContent = mergeState.originalContent;
|
|
1590
|
+
|
|
1591
|
+
// Process conflicts in reverse order to preserve offsets
|
|
1592
|
+
const sortedConflicts = [...mergeState.conflicts].sort((a, b) => b.startOffset - a.startOffset);
|
|
1593
|
+
|
|
1594
|
+
for (const conflict of sortedConflicts) {
|
|
1595
|
+
if (conflict.resolvedContent !== undefined) {
|
|
1596
|
+
finalContent =
|
|
1597
|
+
finalContent.substring(0, conflict.startOffset) +
|
|
1598
|
+
conflict.resolvedContent +
|
|
1599
|
+
finalContent.substring(conflict.endOffset);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Update the original buffer with resolved content
|
|
1604
|
+
if (mergeState.sourceBufferId !== null) {
|
|
1605
|
+
const bufferLength = editor.getBufferLength(mergeState.sourceBufferId);
|
|
1606
|
+
|
|
1607
|
+
// Delete all content
|
|
1608
|
+
if (bufferLength > 0) {
|
|
1609
|
+
editor.deleteRange(mergeState.sourceBufferId, { start: 0, end: bufferLength });
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Insert resolved content
|
|
1613
|
+
editor.insertText(mergeState.sourceBufferId, 0, finalContent);
|
|
1614
|
+
|
|
1615
|
+
editor.debug("Applied resolved content to source buffer");
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Close merge panels
|
|
1619
|
+
closeMergePanels();
|
|
1620
|
+
|
|
1621
|
+
editor.setStatus(editor.t("status.complete"));
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
globalThis.merge_abort = function(): void {
|
|
1625
|
+
if (!mergeState.isActive) {
|
|
1626
|
+
editor.setStatus(editor.t("status.nothing_to_abort"));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// TODO: Add confirmation prompt if there are resolutions
|
|
1631
|
+
|
|
1632
|
+
// Close merge panels without saving
|
|
1633
|
+
closeMergePanels();
|
|
1634
|
+
|
|
1635
|
+
editor.setStatus(editor.t("status.aborted"));
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Close all merge panels and reset state
|
|
1640
|
+
*/
|
|
1641
|
+
function closeMergePanels(): void {
|
|
1642
|
+
// Close buffers
|
|
1643
|
+
if (mergeState.oursPanelId !== null) {
|
|
1644
|
+
editor.closeBuffer(mergeState.oursPanelId);
|
|
1645
|
+
}
|
|
1646
|
+
if (mergeState.theirsPanelId !== null) {
|
|
1647
|
+
editor.closeBuffer(mergeState.theirsPanelId);
|
|
1648
|
+
}
|
|
1649
|
+
if (mergeState.resultPanelId !== null) {
|
|
1650
|
+
editor.closeBuffer(mergeState.resultPanelId);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Close splits
|
|
1654
|
+
if (mergeState.oursSplitId !== null) {
|
|
1655
|
+
editor.closeSplit(mergeState.oursSplitId);
|
|
1656
|
+
}
|
|
1657
|
+
if (mergeState.theirsSplitId !== null) {
|
|
1658
|
+
editor.closeSplit(mergeState.theirsSplitId);
|
|
1659
|
+
}
|
|
1660
|
+
if (mergeState.resultSplitId !== null) {
|
|
1661
|
+
editor.closeSplit(mergeState.resultSplitId);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Focus back on source buffer if it exists
|
|
1665
|
+
if (mergeState.sourceBufferId !== null) {
|
|
1666
|
+
editor.showBuffer(mergeState.sourceBufferId);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Unregister merge-mode commands
|
|
1670
|
+
unregisterMergeModeCommands();
|
|
1671
|
+
|
|
1672
|
+
// Reset state
|
|
1673
|
+
mergeState.isActive = false;
|
|
1674
|
+
mergeState.sourceBufferId = null;
|
|
1675
|
+
mergeState.sourcePath = null;
|
|
1676
|
+
mergeState.originalContent = "";
|
|
1677
|
+
mergeState.conflicts = [];
|
|
1678
|
+
mergeState.selectedIndex = 0;
|
|
1679
|
+
mergeState.oursPanelId = null;
|
|
1680
|
+
mergeState.theirsPanelId = null;
|
|
1681
|
+
mergeState.resultPanelId = null;
|
|
1682
|
+
mergeState.oursSplitId = null;
|
|
1683
|
+
mergeState.theirsSplitId = null;
|
|
1684
|
+
mergeState.resultSplitId = null;
|
|
1685
|
+
mergeState.oursContent = "";
|
|
1686
|
+
mergeState.theirsContent = "";
|
|
1687
|
+
mergeState.baseContent = "";
|
|
1688
|
+
mergeState.resultContent = "";
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// =============================================================================
|
|
1692
|
+
// Public Commands - Help
|
|
1693
|
+
// =============================================================================
|
|
1694
|
+
|
|
1695
|
+
globalThis.merge_show_help = function(): void {
|
|
1696
|
+
editor.setStatus(editor.t("status.help"));
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
// =============================================================================
|
|
1700
|
+
// Hook Handlers - Auto-Detection
|
|
1701
|
+
// =============================================================================
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Handle buffer activation - check for conflict markers
|
|
1705
|
+
*/
|
|
1706
|
+
globalThis.onMergeBufferActivated = async function(data: { buffer_id: number }): Promise<void> {
|
|
1707
|
+
// Don't trigger if already in merge mode
|
|
1708
|
+
if (mergeState.isActive) return;
|
|
1709
|
+
|
|
1710
|
+
// Don't trigger for virtual buffers
|
|
1711
|
+
const info = editor.getBufferInfo(data.buffer_id);
|
|
1712
|
+
if (!info || !info.path) return;
|
|
1713
|
+
|
|
1714
|
+
// Get the directory of the file for running git commands
|
|
1715
|
+
const fileDir = editor.pathDirname(info.path);
|
|
1716
|
+
|
|
1717
|
+
// Check if we're in a git repo first
|
|
1718
|
+
try {
|
|
1719
|
+
const gitCheck = await editor.spawnProcess("git", ["rev-parse", "--is-inside-work-tree"], fileDir);
|
|
1720
|
+
if (gitCheck.exit_code !== 0) return;
|
|
1721
|
+
|
|
1722
|
+
// Check for unmerged entries
|
|
1723
|
+
const lsFiles = await editor.spawnProcess("git", ["ls-files", "-u", info.path], fileDir);
|
|
1724
|
+
if (lsFiles.exit_code === 0 && lsFiles.stdout.trim().length > 0) {
|
|
1725
|
+
editor.setStatus(editor.t("status.detected"));
|
|
1726
|
+
}
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
// Not in git repo or other error, ignore
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Handle file open - check for conflict markers
|
|
1734
|
+
*/
|
|
1735
|
+
globalThis.onMergeAfterFileOpen = async function(data: { buffer_id: number; path: string }): Promise<void> {
|
|
1736
|
+
// Don't trigger if already in merge mode
|
|
1737
|
+
if (mergeState.isActive) return;
|
|
1738
|
+
|
|
1739
|
+
// Get the directory of the file for running git commands
|
|
1740
|
+
const fileDir = editor.pathDirname(data.path);
|
|
1741
|
+
|
|
1742
|
+
// Check if we're in a git repo first
|
|
1743
|
+
try {
|
|
1744
|
+
const gitCheck = await editor.spawnProcess("git", ["rev-parse", "--is-inside-work-tree"], fileDir);
|
|
1745
|
+
if (gitCheck.exit_code !== 0) return;
|
|
1746
|
+
|
|
1747
|
+
// Check for unmerged entries
|
|
1748
|
+
const lsFiles = await editor.spawnProcess("git", ["ls-files", "-u", data.path], fileDir);
|
|
1749
|
+
if (lsFiles.exit_code === 0 && lsFiles.stdout.trim().length > 0) {
|
|
1750
|
+
editor.setStatus(editor.t("status.detected_file", { path: data.path }));
|
|
1751
|
+
}
|
|
1752
|
+
} catch (e) {
|
|
1753
|
+
// Not in git repo or other error, ignore
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// =============================================================================
|
|
1758
|
+
// Hook Registration
|
|
1759
|
+
// =============================================================================
|
|
1760
|
+
|
|
1761
|
+
editor.on("buffer_activated", "onMergeBufferActivated");
|
|
1762
|
+
editor.on("after_file_open", "onMergeAfterFileOpen");
|
|
1763
|
+
|
|
1764
|
+
// =============================================================================
|
|
1765
|
+
// Command Registration - Dynamic based on merge mode state
|
|
1766
|
+
// =============================================================================
|
|
1767
|
+
|
|
1768
|
+
// Commands that are only available during active merge mode
|
|
1769
|
+
const MERGE_MODE_COMMANDS = [
|
|
1770
|
+
{ name: "%cmd.next", desc: "%cmd.next_desc", action: "merge_next_conflict" },
|
|
1771
|
+
{ name: "%cmd.prev", desc: "%cmd.prev_desc", action: "merge_prev_conflict" },
|
|
1772
|
+
{ name: "%cmd.use_ours", desc: "%cmd.use_ours_desc", action: "merge_use_ours" },
|
|
1773
|
+
{ name: "%cmd.take_theirs", desc: "%cmd.take_theirs_desc", action: "merge_take_theirs" },
|
|
1774
|
+
{ name: "%cmd.use_both", desc: "%cmd.use_both_desc", action: "merge_use_both" },
|
|
1775
|
+
{ name: "%cmd.save_exit", desc: "%cmd.save_exit_desc", action: "merge_save_and_exit" },
|
|
1776
|
+
{ name: "%cmd.abort", desc: "%cmd.abort_desc", action: "merge_abort" },
|
|
1777
|
+
];
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Register merge-mode specific commands (called when merge mode starts)
|
|
1781
|
+
*/
|
|
1782
|
+
function registerMergeModeCommands(): void {
|
|
1783
|
+
for (const cmd of MERGE_MODE_COMMANDS) {
|
|
1784
|
+
editor.registerCommand(cmd.name, cmd.desc, cmd.action, "normal");
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Unregister merge-mode specific commands (called when merge mode ends)
|
|
1790
|
+
*/
|
|
1791
|
+
function unregisterMergeModeCommands(): void {
|
|
1792
|
+
for (const cmd of MERGE_MODE_COMMANDS) {
|
|
1793
|
+
editor.unregisterCommand(cmd.name);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Only register "Start Resolution" at plugin load - other commands are registered dynamically
|
|
1798
|
+
editor.registerCommand(
|
|
1799
|
+
"%cmd.start",
|
|
1800
|
+
"%cmd.start_desc",
|
|
1801
|
+
"start_merge_conflict",
|
|
1802
|
+
"normal"
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
// =============================================================================
|
|
1806
|
+
// Plugin Initialization
|
|
1807
|
+
// =============================================================================
|
|
1808
|
+
|
|
1809
|
+
editor.setStatus(editor.t("status.ready"));
|
|
1810
|
+
editor.debug("Merge plugin initialized - Use 'Merge: Start Resolution' for files with conflicts");
|