# Conflict Resolution Specification

## 1. Overview

Pad operates on a directory synced by Syncthing. External modifications to files originate from edits on other devices, delivered via Syncthing. This document specifies how Pad detects, classifies, and resolves such changes.

## 2. Syncthing Conflict File Format

When Syncthing detects that two devices have modified the same file, it preserves both versions:

- **Original file** (`notes.txt`) — the local device's version (what Pad edited)
- **Conflict file** (`notes.sync-conflict-2024-05-15-1430-ABCDEF1.txt`) — the remote device's version

**Naming pattern:** `<base>.sync-conflict-<date>-<time>-<modifiedBy>.<ext>`

| Component | Format | Example |
|---|---|---|
| `base` | Original filename without extension | `notes` |
| `date` | ISO date | `2024-05-15` |
| `time` | 24-hour time, no separator | `1430` |
| `modifiedBy` | Remote device short ID | `ABCDEF1` |

To resolve the original file from a conflict file, strip the `.sync-conflict-<date>-<time>-<modifiedBy>` suffix and reattach the extension.

## 3. Change Classification

When the file system watcher detects a change, Pad classifies it into one of two categories:

| Category | Trigger | Meaning |
|---|---|---|
| **External update** | File content changed on disk, no conflict file exists | Another device edited and synced; our device received the update cleanly |
| **Sync conflict** | A `.sync-conflict-*` file appeared for a file we have open or know about | Both devices edited the same file; Syncthing could not merge automatically |

### Detection Logic

```
ON file_modified(event):
    original = event.path
    conflict = find_conflict_file_for(original)
    
    IF conflict exists:
        → SYNC CONFLICT
    ELSE:
        → EXTERNAL UPDATE

ON new_file(event):
    IF event.path matches conflict pattern:
        original = resolve_original(event.path)
        → SYNC CONFLICT for original
```

## 4. External Update (No Conflict)

The file was modified on another device and synced cleanly. Our local edits (if any) were already synced before the remote edit, so there is no divergence.

### Procedure

1. **Detect** — watcher fires `file_modified` for the active file
2. **Read** — load the new content from disk
3. **Diff** — compute diff between the editor's buffer (old) and disk content (new) to understand what changed
4. **Re-anchor undo stack** — for each operation in the undo stack, attempt to locate its context in the new file content (see §6)
5. **Replace buffer** — swap the edit buffer with the new content
6. **Preserve cursor** — if the cursor position falls within a changed region, move it to the start of the changed region; otherwise keep it at the same byte offset
7. **Continue** — user can edit immediately, undo works with re-anchored operations

### Edge Cases

| Scenario | Behavior |
|---|---|
| User has un-saved edits (within debounce window) | Flush auto-save immediately, then proceed |
| User is actively typing | Complete the current keystroke, then apply update on next detection |
| File was truncated externally | Load empty/new content, reset cursor to 0 |
| Undo operations cannot be re-anchored | Drop unanchorable operations silently (context no longer exists in new content) |

## 5. Sync Conflict

Both devices edited the same file. Syncthing preserved both versions. Pad must help the user integrate the changes.

### Procedure

1. **Detect** — watcher fires `new_file` for a conflict file, or `file_modified` with a corresponding conflict file present
2. **Identify** — resolve the original file path from the conflict file name
3. **Load both versions:**
   - **Ours** (`notes.txt`) — the local file, may contain the user's current edits
   - **Theirs** (`notes.sync-conflict-*.txt`) — the remote device's version
4. **Diff** — compute a line-based diff between ours and theirs, producing a list of conflicting hunks
5. **Present merge UI** — show the user each hunk with resolution options (see §7)
6. **Apply resolution** — produce a merged file from the user's choices
7. **Persist** — write merged content to the original file (`notes.txt`), delete the conflict file
8. **Re-anchor undo stack** — same as external update (§4, step 4)
9. **Replace buffer** — swap edit buffer with merged content
10. **Continue** — user can edit immediately

### Edge Cases

| Scenario | Behavior |
|---|---|
| Conflict file for a file the user never opened | Leave it alone (out of scope; Syncthing will handle on next sync cycle) |
| Multiple conflict files for the same original (rare, from rapid edits) | Resolve the most recent one first; delete all conflict files after resolution |
| User closes the app while merge UI is shown | Persist the unresolved state; reopen merge UI on relaunch |
| Conflict file is deleted by user externally | Treat as abandoned; no action needed |

## 6. Undo Stack Re-anchoring

After any external change or conflict resolution, the undo stack must be reconciled with the new file content.

### Algorithm

For each chain in the undo stack (most recent first):

1. **Locate the head's context** in the new file content:
   - For **insert chains**: search for `before` + `after` pattern (text surrounding the insertion point)
   - For **delete chains**: search for `after` pattern (text after the deletion point)
   
2. **If context is found:**
   - Update the head's `pos` to the new location
   - All tail entries inherit from the re-anchored head
   - Chain is preserved

3. **If context is not found:**
   - Remove the entire chain from the undo stack
   - The text surrounding the operation has changed too much to reliably replay

4. **If context is found multiple times:**
   - Pick the occurrence closest to the original `pos`
   - If ambiguity remains, pick the first occurrence

### Limitations

- Re-anchoring is best-effort. If the external change modified text within a chain's context window, that chain is lost.
- This is acceptable — the alternative (full diff-based rebasing) is significantly more complex and error-prone.

## 7. Merge UI

The merge UI presents conflicting hunks to the user for per-hunk resolution.

### Hunk Structure

Each hunk represents a region where ours and theirs diverge:

```
─── Hunk 3 of 7 ─────────────────────────
   Line 142 — Line 148

Context (unchanged):
    The quick brown fox

Ours:
    jumps over the lazy dog
    The end.

Theirs:
    jumps over the sleepy cat
    A new paragraph begins.
    The end.
```

### Resolution Options (per hunk)

| Choice | Result |
|---|---|
| **Keep ours** | Use our version of this hunk |
| **Keep theirs** | Use their version of this hunk |
| **Merge both** | Concatenate ours + theirs (ours first, then a blank line, then theirs) |

### UI Behavior

- **One hunk at a time** — shown full screen, swipe or tap to navigate between hunks
- **Tap a version** to preview it (highlight in the full file context)
- **Tap a resolution button** to accept and move to next hunk
- **Apply all** button appears after the last hunk is resolved
- **Cancel** — abort merge, keep our version, leave conflict file untouched (will reappear on next sync)

### State Persistence

The merge session (which conflict file, which hunks, which resolutions) is persisted to the state file. If the app is killed during a merge:

1. On relaunch, detect the unresolved conflict file
2. Re-compute the diff (idempotent)
3. Restore any resolutions the user already made (from state file)
4. Present the merge UI from the first unresolved hunk

## 8. Diff Engine

Pad needs a line-based diff engine that produces hunks with context.

### Requirements

- **Line-based** — hunks are groups of lines, not character offsets
- **Streaming** — must not load entire files into memory; process line by line
- **Context-aware** — each hunk includes N context lines before and after the change (configurable, default 3)
- **Positioned** — each hunk reports the starting line number in both versions
- **Efficient** — must handle files with millions of lines without O(n²) behavior

### Output Format

```go
type Hunk struct {
    OurStartLine    int       // Starting line in our version (1-indexed)
    OurLines        []string  // Lines from our version (replaced by TheirLines)
    TheirStartLine  int       // Starting line in their version (1-indexed)
    TheirLines      []string  // Lines from their version (replaces OurLines)
    ContextBefore   []string  // Unchanged lines before the hunk
    ContextAfter    []string  // Unchanged lines after the hunk
}
```

### Implementation Options

| Approach | Pros | Cons |
|---|---|---|
| **Go `textdiff` / `diffmatchpatch`** | Available, battle-tested | Character-based, not line-based; may need wrapping |
| **Shell `diff` invocation** | Standard, line-based, efficient | Requires subprocess, harder to stream on Android |
| **Custom Myers diff** | Full control, line-based, streaming | More code to write and test |
| **Import `github.com/sergi/go-diff`** | Line-based, Go-native | External dependency, may not stream |

**Recommendation:** Import a Go-native line-based diff library (e.g., `go-diff` or `diffmatchpatch` with line-level wrapping). Evaluate during implementation.

## 9. Interaction with Auto-save

| Event | Auto-save behavior |
|---|---|
| External update applied | Auto-save the new buffer content (so our disk copy matches the synced version) |
| Merge in progress | Pause auto-save (buffer is in an inconsistent state) |
| Merge applied | Auto-save the merged content immediately |
| Conflict file detected while auto-save debounce is active | Flush auto-save first, then proceed with conflict detection |

## 10. Out of Scope

- Three-way merge (we don't have access to the common ancestor; Syncthing doesn't preserve it)
- Automatic merge (all changes require user confirmation)
- Binary file conflicts (Pad only handles text files)
- Folder-level conflicts (only file-level)
