Coverage for vibefpmgui/file_diff.py: 84%
58 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""A file's diff with a line over it saying what's compared ("staged →
2working tree", "3f9db510 → 930ec19c"): the Git panel's Diff page and its
3History's. The two versions are read, and compared, on a pool thread; a
4file chosen meanwhile wins (the older answer is dropped).
5"""
7from __future__ import annotations
9from collections.abc import Callable, Hashable
11from qtpy.QtCore import Signal
12from qtpy.QtGui import QFont
13from qtpy.QtWidgets import QVBoxLayout, QWidget
15from .background import call_in_background
16from .diff_view import DiffView, compare
17from .git_status import BINARY
18from .widgets import StatusLabel
20# What load() reads: (the older text, the newer one, what's compared). A text
21# is None where there's no such version (added, deleted), BINARY where it
22# isn't text. It raises OSError, or RuntimeError (git_status.GitError), if
23# it can't be read.
24Versions = tuple[str | None, str | None, str]
27class FileDiff(QWidget):
28 open_requested = Signal(int) # a line of the newer version (1-based), double-clicked
30 def __init__(self, parent=None):
31 super().__init__(parent)
32 self.title = StatusLabel()
33 self.diff = DiffView()
34 self.diff.open_requested.connect(self.open_requested)
35 self._loading: Hashable | None = None # (what's being read: see load)
36 layout = QVBoxLayout(self)
37 layout.setContentsMargins(0, 0, 0, 0)
38 layout.setSpacing(4)
39 layout.addWidget(self.title)
40 layout.addWidget(self.diff, 1)
42 def load(self, key: Hashable, path: str, read: Callable[[], Versions]):
43 """Show ``path``'s diff, its versions from ``read()`` (on a pool
44 thread); ``key`` tells this file apart from the next one chosen."""
45 self._loading = key
46 self.title.setText(f"<b>{path}</b> · reading…")
48 def work():
49 try:
50 old, new, compared = read()
51 except (OSError, RuntimeError) as error:
52 return str(error) or type(error).__name__
53 # (Compared here too: a third of a second for a big file, which the window shouldn't wait for.)
54 return compared, None if BINARY in (old, new) else compare(old, new)
56 call_in_background(work, lambda result: self._read(key, path, result), self)
58 def _read(self, key: Hashable, path: str, result):
59 if key != self._loading:
60 return # (another file chosen meanwhile)
61 if isinstance(result, str): 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true
62 self.title.setText(f"<b>{path}</b> · couldn't read it: {result}")
63 self.diff.clear()
64 return
65 compared, rows = result
66 if rows is None: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true
67 self.title.setText(f"<b>{path}</b> · binary or very big: no diff shown")
68 self.diff.clear()
69 return
70 self.title.setText(f"<b>{path}</b> · {compared}")
71 self.diff.show_rows(rows, path)
73 def clear(self):
74 self._loading = None
75 self.title.setText("")
76 self.diff.clear()
78 def set_unified(self, on: bool):
79 self.diff.set_unified(on)
81 def set_font(self, font: QFont):
82 self.diff.set_font(font)
84 def refresh_style(self, muted: str):
85 """The colors: the title's (muted), the diff's (the syntax style's)."""
86 self.title.setStyleSheet(f"color: {muted};")
87 self.diff.refresh_style()