Skip to content

Commit 36b42f8

Browse files
joskeclaude
andauthored
refactoring + bugfixes (#34)
* Extract shared helpers from diff/merge views and consolidate file watchers Move duplicated code into common.rs: - build_find_bar(): find/replace bar + goto-line + 5 GActions (~220 lines x2) - build_nav_button_group(): prev/next navigation buttons - build_undo_redo_box(): undo/redo buttons wired to active view - build_filter_toggles(): blank-line and whitespace toggle buttons - start_file_watcher(): generic file watcher with on_tick callback - build_app_window(): shared window builder with notebook and actions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Split common.rs (4,419 lines) into 12 focused submodules Convert the monolithic common.rs into a common/ directory module with dedicated files for each responsibility area: editor, gutter, helpers, tabs, find_bar, scroll_sync, navigation, toolbar, search, chunk_map, file_watcher, and tests. Pure code motion with no logic changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Refactor FileTab into Diff/Merge enum with PaneInfo Replace flat FileTab struct with a Diff/Merge enum. Introduce PaneInfo to hold per-pane (path, buffer, save button) state. Accessor methods (saveable_panes, is_reloadable) eliminate dummy save buttons and empty-path guards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Extract shared editor action helpers (refresh, save-as, save-all) Move duplicated refresh/save-as/save-all logic from diff_view and merge_view into shared helpers in common/helpers.rs. Each view still wires its own actions but calls the shared functions for the actual save/reload/dialog logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Deduplicate dir_window button handlers via action group Replace ~140 lines of duplicated copy-left/copy-right/delete button click handlers with one-liner activate_action calls. The action group definitions (already used for keyboard shortcuts) now serve both buttons and keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix merge Save As stale tab path and remove duplicate action group insert - Pass middle_tab_path through MergeViewResult so FileTab::Merge shares the same Rc as the save-as action, keeping close/unsaved dialogs in sync after Save As. - Remove duplicate vcs_tab.insert_action_group call in vcs_window.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix merge tab close prompt showing stale path after Save As Read the live middle_tab_path Rc instead of capturing a static String from second_path at construction time. The confirm_unsaved_dialog now shows the current file name after Save As. The merge watcher intentionally tracks original source paths (not the Save As target) since the watcher monitors source files being merged, matching Meld's behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update TESTING.md with missing test cases Add 42 test cases for features not previously covered: Save As, Save All, Open Externally, Refresh, tab navigation (Ctrl+1..9), New Comparison (Ctrl+N), blank comparison, wrap-around navigation, chunk map viewport indicator, and keyboard shortcuts dialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix error message for non-existent single path argument `path.is_dir()` returns false for non-existent paths, causing a misleading "single file argument not supported" error. Add an explicit existence check before the is_dir/is_file branches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Support -L/--label flags for directory comparison Wire labels through build_dir_window → build_dir_tab to override pane header text. Labels swap correctly with the swap-panes button. Tooltips still show the actual directory path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix CLI help: show <LABEL> instead of <LABELS> for -L flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Repaint chunk borders when clicking inside a chunk Cursor tracking updated current_chunk index but never triggered a redraw, so bold borders only appeared after keyboard navigation or scrolling. Now queue_draw on filler overlays and gutters when the current chunk changes. Fixed in both diff and merge views. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Scroll to cursor after toolbar undo/redo The toolbar undo/redo buttons called buf.undo()/redo() without scrolling the view, causing the viewport to jump away from the edit location. Now calls scroll_mark_onscreen after each operation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update TESTING.md: vertical scroll is synced by chunk Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix find/replace: cross-pane navigation, Escape, macOS shortcut, undo - Find next/prev now cycles across panes when no match in current pane - Escape closes find bar from widget level (not just text views) - Use Cmd+Shift+H for Find & Replace on macOS (Cmd+H is system Hide) - Replace All uses delete+insert in a user action group so undo works Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix find next/prev cycling across panes find_next_match wrapped within the same buffer, so cross-pane search was never reached. Split into find_next_match_no_wrap with a skip_current flag: current pane skips the cursor position, other panes search from start/end without skipping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Clarify TESTING.md wording for Alt+Left/Right in dir file diff tab Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update TESTING.md: directory tab shows dir names, not "Directory" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Show full absolute path in pane header tooltip Canonicalize file_path so the tooltip always shows the full path instead of a relative one when launched with relative arguments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Prevent duplicate file tabs in directory comparison open_file_diff now checks if a tab with the same rel_path is already open and switches to it instead of opening a duplicate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix TESTING.md: tab switching is Alt+1..9, not Ctrl+1..9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use native trash command on macOS instead of GIO GIO's File::trash() silently deletes directories permanently on macOS instead of moving them to Finder's Trash. Use the native macOS trash command which works correctly. GIO is still used on Linux. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix merge view chunk highlighting, conflict drawing, and navigation - Add conflict-aware cursor detection: clicking conflict regions on left/right panes now highlights the conflict band via fallback to merged_gutter_chunks when chunk_at_cursor returns None - Separate conflict drawing from regular chunk drawing: skip conflict chunks in draw_chunk_backgrounds via is_conflict flags, draw them with dedicated conflict stroke functions at full alpha - Add bold border support for conflict regions (current chunk highlight) - Fix navigation guard: prevent cursor_position_notify from overriding current_chunk during Alt+Up/Down navigation - Collapse conflict bands into single navigation targets on side panes so Alt+Up/Down skips entire conflict band in one step - Fix chunk map viewport indicators for merge view by redrawing on all three scroll adjustments - Focus middle pane on 3-way merge open (CLI and new-comparison tab) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix scroll sync skipping Equal regions in line mapping map_line_through_chunks advanced past Equal chunks without checking if the source line fell inside them, causing the mapped position to jump to the next non-equal chunk boundary. This made the left pane "stick" in 3-way merge when left and middle had few differences. Now returns the correct 1:1 mapped position when source line is within an Equal region. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Track Save As directory changes in file watcher for diff and merge views FileWatcher now supports dynamically adding watched paths via watch(). Both file_window and merge_view monitor save paths for directory changes and tell the watcher to pick up new directories after Save As. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Show conflicting files in VCS window and open them in 3-way merge view Add VcsStatus::Conflict for unmerged files (UU, AA, DD, DU, UD, AU, UA). Conflict files display with red "Conflict" label in the VCS file list. Opening a conflict launches a 3-way merge: ours (:2:) on left, working copy in middle, theirs (:3:) on right. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * bump to 0.7.0 * Fix flaky UI tests by retrying keystrokes and polling for label changes Centralize xdotool key-sending in conftest.py with a 200ms delay after focus to let GTK process the event. Add send_keys_until() that retries keystrokes up to 3 times if the expected label change doesn't appear within the timeout (default 1s per attempt, 5s max). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * focus left pane on window show --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8e6ff6a commit 36b42f8

30 files changed

Lines changed: 5968 additions & 5802 deletions

Cargo.lock

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mergers"
3-
version = "0.6.1"
3+
version = "0.7.0"
44
edition = "2024"
55
description = "A visual diff and merge tool for files and directories"
66
license = "GPL-2.0"

TESTING.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- [ ] "3-way Merge" button: three file dialogs, then opens merge; welcome closes
2222
- [ ] Preferences button / Ctrl+,: opens Preferences dialog
2323
- [ ] Ctrl+W: closes welcome window
24+
- [ ] Ctrl+N: opens a new welcome window
2425
- [ ] Cancel any file dialog: nothing happens
2526

2627
## File Diff (2-way)
@@ -32,6 +33,7 @@
3233
- [ ] Filler lines: thin colored lines where the other side has more content
3334
- [ ] Gutter: curved connecting bands between panes
3435
- [ ] Chunk map strips: minimap of changes at far edges; clickable to jump
36+
- [ ] Chunk map viewport indicator: visible rectangle showing current scroll position
3537
- [ ] Syntax highlighting: detected from filename
3638
- [ ] Line numbers: shown/hidden per settings
3739

@@ -55,7 +57,7 @@
5557

5658
### Scroll Sync
5759
- [ ] Horizontal scroll synced between panes
58-
- [ ] Vertical scroll independent per pane
60+
- [ ] Vertical scroll synced by chunk between panes
5961

6062
### Find / Replace
6163
- [ ] Ctrl+F: opens find bar; Ctrl+H: opens find + replace
@@ -68,11 +70,19 @@
6870
- [ ] Close (X / Escape): hides bar, clears highlights
6971

7072
### Save
73+
- [ ] Ctrl+S: saves focused pane; button goes insensitive
7174
- [ ] Save button insensitive until buffer changes
7275
- [ ] Save writes correct file; button goes insensitive; error dialog on failure
7376
- [ ] Save path tracks swaps: after swap, save writes to correct original file
7477
- [ ] Save after swap: pane contents and labels remain consistent (no un-swap)
7578
- [ ] Save after swap in dir file tab: watcher reload preserves swapped state
79+
- [ ] Ctrl+Shift+S (Save As): file dialog; writes to chosen path; updates pane label and tooltip
80+
- [ ] Save As updates tab-tracked path (close/unsaved dialog shows new name)
81+
- [ ] Ctrl+Shift+L (Save All): saves all dirty panes in current view
82+
- [ ] Ctrl+Shift+O: opens focused file in system default app
83+
- [ ] Ctrl+R / F5 (Refresh): reloads both files from disk
84+
- [ ] Refresh with unsaved changes: confirm dialog before reloading
85+
- [ ] Refresh on blank comparison pane: no-op (doesn't crash)
7686

7787
### File Watcher
7888
- [ ] External file change: auto-reloads if no unsaved changes
@@ -91,7 +101,7 @@
91101
- [ ] Alt+Right on a diff chunk: copies left chunk content to right (no file copy dialog)
92102
- [ ] Alt+Left on a diff chunk: copies right chunk content to left (no file copy dialog)
93103
- [ ] Alt+Left/Right with no current chunk: nothing happens
94-
- [ ] In dir window file tab: Alt+Left/Right copies chunks, does NOT trigger dir file copy
104+
- [ ] In file diff tab opened from dir window: Alt+Left/Right copies chunks, does NOT trigger dir file copy
95105

96106
### Edge Cases
97107
- [ ] Identical files: "Files are identical" info bar; edit one side -> bar disappears; undo -> reappears
@@ -110,6 +120,8 @@
110120
- [ ] Click in pane, then Alt+Down: navigates from cursor position, not from top
111121
- [ ] Navigate chunks with cursor in different positions: cursor-aware seeking
112122
- [ ] Alt+Up/Down intercepted: sourceview move-lines action does not fire
123+
- [ ] Wrap-around navigation: Alt+Down past last chunk wraps to first (if enabled in prefs)
124+
- [ ] Wrap-around off: Alt+Down on last chunk stays; Alt+Up on first chunk stays
113125

114126
## Directory Comparison
115127

@@ -143,13 +155,19 @@
143155
- [ ] "Delete": always available
144156

145157
### Notebook Tabs
146-
- [ ] Directory tab labeled "Directory"
158+
- [ ] Directory tab labeled with "dir1 — dir2"
147159
- [ ] File tabs labeled with dir-relative names
148160
- [ ] Opening file diff tab: left pane has focus, cursor at line 1
149161
- [ ] Tab close (X): prompts for unsaved changes
150162
- [ ] Ctrl+W on Directory tab: closes entire window
151163
- [ ] Ctrl+W on file tab: closes that tab
152164
- [ ] Same file not opened twice: switches to existing tab
165+
- [ ] Alt+1..9: switches to tab by index
166+
- [ ] Ctrl+N: opens New Comparison tab
167+
- [ ] New Comparison tab: "Compare Files" and "3-Way Merge" buttons with file choosers
168+
- [ ] New Comparison → Compare Files: opens diff tab, removes new-comparison tab
169+
- [ ] New Comparison → 3-Way Merge: opens merge tab, removes new-comparison tab
170+
- [ ] Blank comparison (Ctrl+T or from New Comparison): opens editable blank diff tab
153171

154172
### File Watcher
155173
- [ ] Directory rescans on FS changes (500ms poll)
@@ -196,13 +214,23 @@
196214

197215
### Scroll Sync
198216
- [ ] Horizontal sync across all three panes
199-
- [ ] Vertical independent per pane
217+
- [ ] Vertical scroll synced by chunk across all three panes
200218

201219
### Find / Replace
202220
- [ ] Ctrl+F/H: find bar highlights matches across all three buffers
203221
- [ ] Match count sums all three buffers
204222
- [ ] Replace All replaces in all three buffers
205223

224+
### Save
225+
- [ ] Ctrl+S: saves middle pane
226+
- [ ] Ctrl+Shift+S (Save As): saves middle to a new path; updates pane label
227+
- [ ] Save As updates tab-tracked path (close/unsaved dialog shows new name)
228+
- [ ] Ctrl+Shift+L (Save All): saves middle if dirty
229+
- [ ] Ctrl+Shift+O: opens focused file externally (works for all 3 panes)
230+
- [ ] Ctrl+R / F5 (Refresh): reloads all 3 files from disk
231+
- [ ] Refresh with unsaved middle: confirm dialog before reloading
232+
- [ ] After Save As, watcher reloads from new path (left/right still watch originals)
233+
206234
### Edge Cases
207235
- [ ] Binary files: all panes show info bar; all read-only; middle save hidden
208236
- [ ] All three identical: no changes, no chunks
@@ -242,6 +270,8 @@
242270
- [ ] Ctrl+W on file tab: closes tab with unsaved check
243271
- [ ] Window close: checks all tabs for unsaved changes
244272
- [ ] Temp directory cleaned up on window destroy
273+
- [ ] Alt+1..9: switches to tab by index
274+
- [ ] Ctrl+N: opens New Comparison tab
245275

246276
### Security
247277
- [ ] Paths with `..` components refused
@@ -255,6 +285,7 @@
255285
- [ ] Word wrap (None/Word/Character): dropdown; live-apply
256286
- [ ] Tab width (1-16): spin button; live-apply
257287
- [ ] Hide hidden files: toggle; default on; saved to settings
288+
- [ ] Wrap-around navigation: toggle; saved to settings
258289
- [ ] File filters: add/remove entries; saved on dialog close
259290
- [ ] Settings persisted to `~/.config/mergers/settings.toml`
260291
- [ ] Changes applied to all views in the parent window
@@ -281,14 +312,25 @@
281312
| Ctrl+J | Previous conflict (merge only) |
282313
| Ctrl+K | Next conflict (merge only) |
283314
| Ctrl+F | Find |
284-
| Ctrl+H | Find + Replace |
315+
| Ctrl+H (Cmd+Shift+H on macOS) | Find + Replace |
285316
| F3 | Find next |
286317
| Shift+F3 | Find previous |
287318
| Ctrl+L | Go to line |
288319
| Ctrl+Shift+P | Export patch (file diff only) |
289320
| Alt+Left | Copy chunk right→left (file diff) / Copy file to left (dir) |
290321
| Alt+Right | Copy chunk left→right (file diff) / Copy file to right (dir) |
291322
| Ctrl+S | Save active pane |
323+
| Ctrl+Shift+S | Save As (file diff / merge) |
324+
| Ctrl+Shift+L | Save All dirty panes |
325+
| Ctrl+Shift+O | Open focused file externally |
326+
| Ctrl+R / F5 | Refresh (reload from disk) |
327+
| Ctrl+N | New Comparison tab (dir/VCS windows) |
328+
| Alt+1..9 | Switch to tab by index (notebook windows) |
292329
| Delete | Delete selected (dir only) |
293330
| Enter | Open selected file / expand-collapse dir (dir/VCS) |
294331
| Escape | Close find bar / go-to-line / preferences dialog |
332+
333+
## Preferences — Keyboard Shortcuts
334+
335+
- [ ] "Keyboard Shortcuts" button in Preferences: opens shortcuts dialog
336+
- [ ] Dialog lists all shortcuts grouped by category

src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct Cli {
1515
paths: Vec<PathBuf>,
1616

1717
/// Custom labels for panes (one per pane)
18-
#[arg(short = 'L', long = "label")]
18+
#[arg(short = 'L', long = "label", value_name = "LABEL")]
1919
labels: Vec<String>,
2020
}
2121

@@ -68,7 +68,10 @@ fn main() -> glib::ExitCode {
6868
}
6969
} else if cli.paths.len() == 1 {
7070
let path = &cli.paths[0];
71-
if path.is_dir() && vcs::is_git_repo(path) {
71+
if !path.exists() {
72+
eprintln!("Error: '{}' does not exist", path.display());
73+
std::process::exit(1);
74+
} else if path.is_dir() && vcs::is_git_repo(path) {
7275
CompareMode::Vcs { dir: path.clone() }
7376
} else if path.is_dir() {
7477
eprintln!("Error: '{}' is not inside a git repository", path.display());

0 commit comments

Comments
 (0)