Skip to content

Commit 36c5936

Browse files
committed
Merge branch 'syncpoints' into 'master'
[FIX] Improve synchronization points Closes #381 See merge request GNOME/meld!80
2 parents d087e57 + 62b932b commit 36c5936

4 files changed

Lines changed: 267 additions & 32 deletions

File tree

help/C/syncpoints.page

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<page xmlns="http://projectmallard.org/1.0/"
2+
type="topic"
3+
id="syncpoints">
4+
<info>
5+
<title type="sort">2</title>
6+
<link type="guide" xref="index#file-mode"/>
7+
<revision docversion="1.6" status="draft"/>
8+
<include href="legal.xml" xmlns="http://www.w3.org/2001/XInclude"/>
9+
<credit type="author copyright">
10+
<name>Roberto Vidal</name>
11+
<email>vidal.roberto.j@gmail.com</email>
12+
<years>2022</years>
13+
</credit>
14+
</info>
15+
16+
<title>Synchronization Points</title>
17+
18+
<p>
19+
Synchronization points help <app>Meld</app> perform a more fine-grained comparison between your files. When a synchronization point is added to each file, <app>Meld</app> effectively performs two comparisons: one for the chunk above the synchronization point and another for the one below.
20+
</p>
21+
22+
<p>
23+
To add a synchronization point, click on a line and then right-click and select <gui style="menu">Add Synchronization Point</gui>. Repeat this with each file in your comparison: click on a line and then right-click and select <gui style="menu">Match Synchronization Point</gui>. Once every file has a synchronization point, they will match each other and the comparison will be updated to take them into account.
24+
</p>
25+
26+
<p>
27+
You can add successive synchronization points and subdivide your files even further by repeating the steps above. Note that synchronization points are matched to each other in the order they appear in the text, which might not correspond to the order they were created.
28+
</p>
29+
30+
<p>
31+
To remove all synchronization points from a comparison, right-click anywhere in the file and select <gui style="menu">Clear Synchronization Points</gui>.
32+
</p>
33+
34+
35+
<section id="dangling-syncpoints">
36+
<title>Dangling and matched syncpoints</title>
37+
38+
<p>
39+
A synchronization point is "dangling" if it has not been matched yet to other files. For instance, if you have 1 synchronization point in each file, they are all matched together and affect the comparison. If you now add a synchronization point to the first file, it is dangling until we add a second synchronization point to the rest of the files.
40+
</p>
41+
42+
<p>
43+
Dangling synchronization points can be moved around: click on a different line, and then right-click and select <gui style="menu">Move Synchronization Point</gui>. Dangling synchronization points can be removed: click on the line where it was set, right-click and select <gui style="menu">Remove Synchronization Point</gui>.
44+
</p>
45+
46+
<p>
47+
Matched synchronization points can be removed as well. Note that removing a matched synchronization point also removes its siblings in the other files. To remove a matched synchronization point, click on the line where it is set, then right-click and select <gui style="menu">Remove Synchronization Point</gui>.
48+
</p>
49+
50+
</section>
51+
52+
</page>

meld/filediff.py

Lines changed: 215 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import functools
1919
import logging
2020
import math
21+
from enum import Enum
2122
from typing import Optional, Tuple, Type
2223

2324
from gi.repository import Gdk, Gio, GLib, GObject, Gtk, GtkSource
@@ -43,6 +44,7 @@
4344
BufferLines,
4445
)
4546
from meld.melddoc import ComparisonState, MeldDoc, open_files_external
47+
from meld.menuhelpers import replace_menu_section
4648
from meld.misc import user_critical, with_focused_pane
4749
from meld.patchdialog import PatchDialog
4850
from meld.recent import RecentType
@@ -281,7 +283,11 @@ def __init__(
281283
self._sync_hscroll_lock = False
282284
self.linediffer = self.differ()
283285
self.force_highlight = False
284-
self.syncpoints = []
286+
287+
def get_mark_line(pane, mark):
288+
return self.textbuffer[pane].get_iter_at_mark(mark).get_line()
289+
290+
self.syncpoints = Syncpoints(num_panes, get_mark_line)
285291
self.in_nested_textview_gutter_expose = False
286292
self._cached_match = CachedSequenceMatcher(self.scheduler)
287293

@@ -313,6 +319,7 @@ def __init__(
313319
# Manually handle GAction additions
314320
actions = (
315321
('add-sync-point', self.add_sync_point),
322+
('remove-sync-point', self.remove_sync_point),
316323
('clear-sync-point', self.clear_sync_points),
317324
('copy', self.action_copy),
318325
('copy-full-path', self.action_copy_full_path),
@@ -359,8 +366,8 @@ def __init__(
359366

360367
builder = Gtk.Builder.new_from_resource(
361368
'/org/gnome/meld/ui/filediff-menus.ui')
362-
context_menu = builder.get_object('filediff-context-menu')
363-
self.popup_menu = Gtk.Menu.new_from_model(context_menu)
369+
self.popup_menu_model = builder.get_object('filediff-context-menu')
370+
self.popup_menu = Gtk.Menu.new_from_model(self.popup_menu_model)
364371
self.popup_menu.attach_to_widget(self)
365372

366373
builder = Gtk.Builder.new_from_resource(
@@ -1392,6 +1399,9 @@ def on_textview_popup_menu(self, textview):
13921399
rect.x, rect.y = textview.buffer_to_window_coords(
13931400
Gtk.TextWindowType.WIDGET, location.x, location.y)
13941401

1402+
pane = self.textview.index(textview)
1403+
self.set_syncpoint_menuitem(pane)
1404+
13951405
self.popup_menu.popup_at_rect(
13961406
Gtk.Widget.get_window(textview),
13971407
rect,
@@ -1405,10 +1415,61 @@ def on_textview_popup_menu(self, textview):
14051415
def on_textview_button_press_event(self, textview, event):
14061416
if event.button == 3:
14071417
textview.grab_focus()
1418+
pane = self.textview.index(textview)
1419+
self.set_syncpoint_menuitem(pane)
14081420
self.popup_menu.popup_at_pointer(event)
14091421
return True
14101422
return False
14111423

1424+
def set_syncpoint_menuitem(self, pane):
1425+
menu_actions = {
1426+
SyncpointAction.ADD: [
1427+
_("Add Synchronization Point"),
1428+
"view.add-sync-point"
1429+
],
1430+
SyncpointAction.DELETE: [
1431+
_("Remove Synchronization Point"),
1432+
"view.remove-sync-point"
1433+
],
1434+
SyncpointAction.MOVE: [
1435+
_("Move Synchronization Point"),
1436+
"view.add-sync-point"
1437+
],
1438+
SyncpointAction.MATCH: [
1439+
_("Match Synchronization Point"),
1440+
"view.add-sync-point"
1441+
],
1442+
SyncpointAction.DISABLED: [
1443+
_("Add Synchronization Point"),
1444+
"view.add-sync-point"
1445+
],
1446+
}
1447+
1448+
def get_mark():
1449+
return self.textbuffer[pane].get_insert()
1450+
1451+
action = self.syncpoints.action(pane, get_mark)
1452+
1453+
self.set_action_enabled(
1454+
"add-sync-point",
1455+
action != SyncpointAction.DISABLED
1456+
)
1457+
1458+
label, action_id = menu_actions[action]
1459+
1460+
syncpoint_menu = Gio.Menu()
1461+
syncpoint_menu.append(label=label, detailed_action=action_id)
1462+
syncpoint_menu.append(
1463+
label=_("Clear Synchronization Points"),
1464+
detailed_action='view.clear-sync-point',
1465+
)
1466+
section = Gio.MenuItem.new_section(None, syncpoint_menu)
1467+
section.set_attribute([("id", "s", "syncpoint-section")])
1468+
replace_menu_section(self.popup_menu_model, section)
1469+
1470+
self.popup_menu = Gtk.Menu.new_from_model(self.popup_menu_model)
1471+
self.popup_menu.attach_to_widget(self)
1472+
14121473
def set_labels(self, labels):
14131474
labels = labels[:self.num_panes]
14141475
for label, buf in zip(labels, self.textbuffer):
@@ -2411,18 +2472,24 @@ def delete_chunk(self, src, chunk):
24112472

24122473
@with_focused_pane
24132474
def add_sync_point(self, pane, *args):
2414-
# Find a non-complete syncpoint, or create a new one
2415-
if self.syncpoints and None in self.syncpoints[-1]:
2416-
syncpoint = self.syncpoints.pop()
2417-
else:
2418-
syncpoint = [None] * self.num_panes
24192475
cursor_it = self.textbuffer[pane].get_iter_at_mark(
24202476
self.textbuffer[pane].get_insert())
2421-
syncpoint[pane] = self.textbuffer[pane].create_mark(None, cursor_it)
2422-
self.syncpoints.append(syncpoint)
24232477

2478+
self.syncpoints.add(
2479+
pane,
2480+
self.textbuffer[pane].create_mark(None, cursor_it)
2481+
)
2482+
2483+
self.refresh_sync_points()
2484+
2485+
@with_focused_pane
2486+
def remove_sync_point(self, pane, *args):
2487+
self.syncpoints.remove(pane, self.textbuffer[pane].get_insert())
2488+
self.refresh_sync_points()
2489+
2490+
def refresh_sync_points(self):
24242491
for i, t in enumerate(self.textview[:self.num_panes]):
2425-
t.syncpoints = [p[i] for p in self.syncpoints if p[i] is not None]
2492+
t.syncpoints = self.syncpoints.points(i)
24262493

24272494
def make_line_retriever(pane, marks):
24282495
buf = self.textbuffer[pane]
@@ -2432,7 +2499,8 @@ def get_line_for_mark():
24322499
return buf.get_iter_at_mark(mark).get_line()
24332500
return get_line_for_mark
24342501

2435-
valid_points = [p for p in self.syncpoints if all(p)]
2502+
valid_points = self.syncpoints.valid_points()
2503+
24362504
if valid_points and self.num_panes == 2:
24372505
self.linediffer.syncpoints = [
24382506
((make_line_retriever(1, p), make_line_retriever(0, p)), )
@@ -2444,6 +2512,8 @@ def get_line_for_mark():
24442512
(make_line_retriever(1, p), make_line_retriever(2, p)))
24452513
for p in valid_points
24462514
]
2515+
elif not valid_points:
2516+
self.linediffer.syncpoints = []
24472517

24482518
if valid_points:
24492519
for mgr in self.msgarea_mgr:
@@ -2460,7 +2530,7 @@ def get_line_for_mark():
24602530
self.refresh_comparison()
24612531

24622532
def clear_sync_points(self, *args):
2463-
self.syncpoints = []
2533+
self.syncpoints.clear()
24642534
self.linediffer.syncpoints = []
24652535
for t in self.textview:
24662536
t.syncpoints = []
@@ -2471,3 +2541,135 @@ def clear_sync_points(self, *args):
24712541

24722542

24732543
FileDiff.set_css_name('meld-file-diff')
2544+
2545+
2546+
class SyncpointAction(Enum):
2547+
# A dangling syncpoint can be moved to the line
2548+
MOVE = "move"
2549+
# A dangling syncpoint sits can be remove from this line
2550+
DELETE = "delete"
2551+
# A syncpoint can be added to this line to match existing ones
2552+
# in other panes
2553+
MATCH = "match"
2554+
# A new, dangling syncpoint can be added to this line
2555+
ADD = "add"
2556+
# No syncpoint-related action can be taken on this line
2557+
DISABLED = "disabled"
2558+
2559+
2560+
class Syncpoints:
2561+
def __init__(self, num_panes: int, comparator):
2562+
self._num_panes = num_panes
2563+
self._points = [[] for _i in range(0, num_panes)]
2564+
self._comparator = comparator
2565+
2566+
def add(self, pane_idx: int, point):
2567+
pane_state = self._pane_state(pane_idx)
2568+
2569+
if pane_state == self.PaneState.DANGLING:
2570+
self._points[pane_idx].pop()
2571+
2572+
self._points[pane_idx].append(point)
2573+
2574+
lengths = set(len(p) for p in self._points)
2575+
2576+
if len(lengths) == 1:
2577+
for (i, p) in enumerate(self._points):
2578+
p.sort(key=lambda point: self._comparator(i, point))
2579+
2580+
def remove(self, pane_idx: int, cursor_point):
2581+
cursor_key = self._comparator(pane_idx, cursor_point)
2582+
2583+
index = -1
2584+
2585+
for (i, point) in enumerate(self._points[pane_idx]):
2586+
if self._comparator(pane_idx, point) == cursor_key:
2587+
index = i
2588+
break
2589+
2590+
assert index is not None
2591+
2592+
pane_state = self._pane_state(pane_idx)
2593+
2594+
assert pane_state != self.PaneState.SHORT
2595+
2596+
if pane_state == self.PaneState.MATCHED:
2597+
for pane in self._points:
2598+
pane.pop(index)
2599+
elif pane_state == self.PaneState.DANGLING:
2600+
self._points[pane_idx].pop()
2601+
2602+
def clear(self):
2603+
self._points = [[] for _i in range(0, self._num_panes)]
2604+
2605+
def points(self, pane_idx: int):
2606+
return self._points[pane_idx].copy()
2607+
2608+
def valid_points(self):
2609+
num_matched = min(len(p) for p in self._points)
2610+
2611+
if not num_matched:
2612+
return []
2613+
2614+
matched = [p[:num_matched] for p in self._points]
2615+
2616+
return [
2617+
tuple(matched_point[i] for matched_point in matched)
2618+
for i in range(0, num_matched)
2619+
]
2620+
2621+
def _pane_state(self, pane_idx: int):
2622+
lengths = set(len(points) for points in self._points)
2623+
2624+
if len(lengths) == 1:
2625+
return self.PaneState.MATCHED
2626+
2627+
if len(self._points[pane_idx]) == min(lengths):
2628+
return self.PaneState.SHORT
2629+
else:
2630+
return self.PaneState.DANGLING
2631+
2632+
def action(self, pane_idx: int, get_mark):
2633+
state = self._pane_state(pane_idx)
2634+
2635+
if state == self.PaneState.SHORT:
2636+
return SyncpointAction.MATCH
2637+
2638+
target = self._comparator(pane_idx, get_mark())
2639+
2640+
points = self._points[pane_idx]
2641+
2642+
if state == self.PaneState.MATCHED:
2643+
is_syncpoint = any(
2644+
self._comparator(pane_idx, point) == target
2645+
for point in points
2646+
)
2647+
2648+
if is_syncpoint:
2649+
return SyncpointAction.DELETE
2650+
else:
2651+
return SyncpointAction.ADD
2652+
2653+
# state == DANGLING
2654+
if target == self._comparator(pane_idx, points[-1]):
2655+
return SyncpointAction.DELETE
2656+
2657+
is_syncpoint = any(
2658+
self._comparator(pane_idx, point) == target
2659+
for point in points
2660+
)
2661+
2662+
if is_syncpoint:
2663+
return SyncpointAction.DISABLED
2664+
else:
2665+
return SyncpointAction.MOVE
2666+
2667+
class PaneState(Enum):
2668+
# The state of a pane with all its syncpoints matched
2669+
MATCHED = "matched"
2670+
# The state of a pane waiting to be matched to existing syncpoints
2671+
# in other panes
2672+
SHORT = "short"
2673+
# The state of a pane with a dangling syncpoint, not yet matched
2674+
# across all panes
2675+
DANGLING = "DANGLING"

meld/resources/gtk/menus.ui

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,6 @@
8686
<attribute name="action">view.merge-all</attribute>
8787
</item>
8888
</section>
89-
<section>
90-
<attribute name="id">synchronisation-section</attribute>
91-
<item>
92-
<attribute name="label" translatable="yes">Add Synchronization Point</attribute>
93-
<attribute name="action">view.add-sync-point</attribute>
94-
</item>
95-
<item>
96-
<attribute name="label" translatable="yes">Clear Synchronization Points</attribute>
97-
<attribute name="action">view.clear-sync-point</attribute>
98-
</item>
99-
</section>
10089
<section>
10190
<attribute name="id">tool-section</attribute>
10291
<item>

0 commit comments

Comments
 (0)