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

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""" 

6 

7from __future__ import annotations 

8 

9from collections.abc import Callable, Hashable 

10 

11from qtpy.QtCore import Signal 

12from qtpy.QtGui import QFont 

13from qtpy.QtWidgets import QVBoxLayout, QWidget 

14 

15from .background import call_in_background 

16from .diff_view import DiffView, compare 

17from .git_status import BINARY 

18from .widgets import StatusLabel 

19 

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] 

25 

26 

27class FileDiff(QWidget): 

28 open_requested = Signal(int) # a line of the newer version (1-based), double-clicked 

29 

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) 

41 

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…") 

47 

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) 

55 

56 call_in_background(work, lambda result: self._read(key, path, result), self) 

57 

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) 

72 

73 def clear(self): 

74 self._loading = None 

75 self.title.setText("") 

76 self.diff.clear() 

77 

78 def set_unified(self, on: bool): 

79 self.diff.set_unified(on) 

80 

81 def set_font(self, font: QFont): 

82 self.diff.set_font(font) 

83 

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()