gitpane 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,134 @@
1
+ """Keep source characters intact through the line-break normalization Qt applies to a document."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from bisect import bisect_left, bisect_right
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QMimeData
10
+ from PySide6.QtGui import QTextCursor
11
+ from PySide6.QtWidgets import QPlainTextEdit
12
+
13
+ from gitpane.history.revision_file import utf16_length
14
+ from gitpane.text.text_query import TextQuery
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Iterator
18
+
19
+
20
+ class SourceTextEdit(QPlainTextEdit):
21
+ """Map a read-only Qt document back to its literal source for search and copy."""
22
+
23
+ def __init__(self) -> None:
24
+ """Track LF line starts independently of embedded Unicode paragraphs."""
25
+ super().__init__()
26
+ self.source_text = ""
27
+ self.source_lines = [""]
28
+ self.line_positions = [0]
29
+ self.collapsed_positions: list[int] = []
30
+ self.source_cr_positions: list[int] = []
31
+ self.source_length = 0
32
+ self.setReadOnly(True)
33
+ self.setUndoRedoEnabled(False)
34
+
35
+ def setPlainText(self, text: str) -> None:
36
+ """Replace source and its position index before Qt formats the document."""
37
+ SourceTextEdit.clear(self)
38
+ self.append_source_text(text)
39
+
40
+ def clear(self) -> None:
41
+ """Discard source mappings together with the document."""
42
+ self.source_text = ""
43
+ self.source_lines = [""]
44
+ self.line_positions = [0]
45
+ self.collapsed_positions.clear()
46
+ self.source_cr_positions.clear()
47
+ self.source_length = 0
48
+ super().clear()
49
+
50
+ def append_source_text(self, text: str) -> None:
51
+ """Append a batch without splitting CRLF pairs between calls."""
52
+ source_position = self.source_length
53
+ document_position = source_position - len(self.collapsed_positions)
54
+ previous = 0
55
+ for match in re.finditer("\r\n|\n", text):
56
+ prefix_length = utf16_length(text[previous:match.start()])
57
+ source_position += prefix_length
58
+ document_position += prefix_length
59
+ if match.group() == "\r\n":
60
+ self.source_cr_positions.append(source_position)
61
+ self.collapsed_positions.append(document_position)
62
+ source_position += len(match.group())
63
+ document_position += 1
64
+ self.line_positions.append(document_position)
65
+ previous = match.end()
66
+ self.source_length += utf16_length(text)
67
+ self.source_text += text
68
+ first_index = 0
69
+ lines = text.split("\n")
70
+ last_line_index = len(self.source_lines) - 1
71
+ self.source_lines[last_line_index] += lines[first_index]
72
+ self.source_lines.extend(lines[first_index + 1:])
73
+ cursor = QTextCursor(self.document())
74
+ cursor.movePosition(QTextCursor.MoveOperation.End)
75
+ cursor.insertText(text)
76
+
77
+ def toPlainText(self) -> str:
78
+ """Return the literal source, including Unicode separators and CRLF."""
79
+ return self.source_text
80
+
81
+ def logical_line_index(self, position: int) -> int:
82
+ """Locate the Git LF line containing a Qt cursor or continuation block."""
83
+ return bisect_right(self.line_positions, position) - 1
84
+
85
+ def source_position(self, position: int) -> int:
86
+ """Translate a document boundary to the original UTF-16 source boundary."""
87
+ return position + bisect_left(self.collapsed_positions, position)
88
+
89
+ def document_position(self, source_position: int) -> int:
90
+ """Translate an original UTF-16 boundary to Qt's normalized document."""
91
+ return source_position - bisect_left(self.source_cr_positions, source_position)
92
+
93
+ def selected_source_text(self, cursor: QTextCursor) -> str:
94
+ """Recover exact selected characters that Qt would otherwise normalize."""
95
+ units_per_character = 2
96
+ start = self.source_position(cursor.selectionStart()) * units_per_character
97
+ end = self.source_position(cursor.selectionEnd()) * units_per_character
98
+ return self.source_text.encode("utf-16-le")[start:end].decode("utf-16-le")
99
+
100
+ def createMimeDataFromSelection(self) -> QMimeData:
101
+ """Copy literal source rather than Qt's substituted paragraph characters."""
102
+ data = QMimeData()
103
+ data.setText(self.selected_source_text(self.textCursor()))
104
+ return data
105
+
106
+ def find_source(self, query: str, start: int, backward: bool = False) -> QTextCursor:
107
+ """Find literal Unicode text across Qt blocks using source UTF-16 offsets."""
108
+ selected = None
109
+ for begin, end in self.find_match_range(query, overlap=True):
110
+ if backward and end <= start:
111
+ selected = (begin, end)
112
+ elif not backward and begin >= start:
113
+ selected = (begin, end)
114
+ break
115
+ if selected is None:
116
+ return QTextCursor()
117
+ begin, end = selected
118
+ cursor = QTextCursor(self.document())
119
+ cursor.setPosition(begin)
120
+ cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
121
+ return cursor
122
+
123
+ def find_match_range(self, query: str, *, overlap: bool = False, wildcard: bool = False, wildcard_whitespace: bool = True) -> Iterator[tuple[int, int]]:
124
+ """Map literal or wildcard matches to Qt coordinates while preserving source separators."""
125
+ if not query:
126
+ return
127
+ previous = 0
128
+ source_position = 0
129
+ for match_begin, match_end in TextQuery(query, wildcard=wildcard).find_match(self.source_text, overlap=overlap, wildcard_whitespace=wildcard_whitespace):
130
+ source_position += utf16_length(self.source_text[previous:match_begin])
131
+ begin = self.document_position(source_position)
132
+ end = self.document_position(source_position + utf16_length(self.source_text[match_begin:match_end]))
133
+ previous = match_begin
134
+ yield begin, end
@@ -0,0 +1,151 @@
1
+ """Read historical source text in a view with line numbers, coloured by bounded syntax decoration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from time import monotonic
6
+ from typing import TYPE_CHECKING
7
+
8
+ from pygments.lexers import get_lexer_for_filename
9
+ from pygments.styles import get_style_by_name
10
+ from pygments.util import ClassNotFound
11
+ from PySide6.QtCore import QEvent, Qt, QTimer
12
+ from PySide6.QtGui import QPainter, QPalette, QSyntaxHighlighter
13
+ from PySide6.QtWidgets import QWidget
14
+
15
+ from gitpane.config import FILE_PARSE_BUDGET_MS, LAYOUT_SPACING
16
+ from gitpane.history.revision_file import utf16_length
17
+ from gitpane.ui.look.theme_color import line_number_gutter_color
18
+ from gitpane.ui.reader.source_text import SourceTextEdit
19
+ from gitpane.ui.reader.syntax_span import SyntaxLine, syntax_spans
20
+
21
+ if TYPE_CHECKING:
22
+ from PySide6.QtGui import QPaintEvent, QResizeEvent
23
+
24
+
25
+ class SourceHighlighter(QSyntaxHighlighter):
26
+ """Cache syntax per LF line, including Qt continuation paragraphs."""
27
+
28
+ def __init__(self, view: SourceFileView) -> None:
29
+ """Keep formatting separate from the source and its copy/search mapping."""
30
+ super().__init__(view.document())
31
+ self.view = view
32
+ self.lexer = None
33
+ self.cached_by_line_index: dict[int, tuple[str, SyntaxLine]] = {}
34
+
35
+ def highlightBlock(self, text: str) -> None:
36
+ """Project cached syntax onto the current Qt block without changing text."""
37
+ if self.lexer is None:
38
+ return
39
+ block = self.currentBlock()
40
+ line_index = self.view.logical_line_index(block.position())
41
+ source = self.view.source_lines[line_index]
42
+ cached = self.cached_by_line_index.get(line_index)
43
+ cached_source, _spans = cached if cached is not None else (None, [])
44
+ if cached_source != source:
45
+ cached = (source, syntax_spans(source, self.lexer, self.view.syntax_style))
46
+ self.cached_by_line_index[line_index] = cached
47
+ _source, spans = cached
48
+ block_offset = block.position() - self.view.line_positions[line_index]
49
+ for start, length, token_format in spans.block_spans(block_offset, utf16_length(text)):
50
+ self.setFormat(start, length, token_format)
51
+
52
+
53
+ class SourceNumberMargin(QWidget):
54
+ """Number logical source lines outside selectable file content."""
55
+
56
+ def __init__(self, view: SourceFileView) -> None:
57
+ """Follow the source reader's scrolling and font metrics."""
58
+ super().__init__(view)
59
+ self.view = view
60
+
61
+ def paintEvent(self, event: QPaintEvent) -> None:
62
+ """Paint visible LF line starts without renumbering Unicode continuations."""
63
+ painter = QPainter(self)
64
+ painter.fillRect(event.rect(), line_number_gutter_color(self.palette()))
65
+ painter.setFont(self.view.font())
66
+ painter.setPen(self.palette().color(QPalette.ColorRole.Text))
67
+ block = self.view.firstVisibleBlock()
68
+ while block.isValid():
69
+ top = round(self.view.blockBoundingGeometry(block).translated(self.view.contentOffset()).top())
70
+ if top > event.rect().bottom():
71
+ break
72
+ line_index = self.view.logical_line_index(block.position())
73
+ if block.isVisible() and block.position() == self.view.line_positions[line_index]:
74
+ height = self.view.fontMetrics().height()
75
+ painter.drawText(0, top, self.width() - LAYOUT_SPACING, height, Qt.AlignmentFlag.AlignRight, str(line_index + 1))
76
+ block = block.next()
77
+
78
+
79
+ class SourceFileView(SourceTextEdit):
80
+ """Read historical code with literal search, native numbering and safe syntax."""
81
+
82
+ def __init__(self) -> None:
83
+ """Attach source decoration while leaving progressive insertion to its owner."""
84
+ super().__init__()
85
+ self.syntax_style = get_style_by_name("default")
86
+ self.syntax_generation = 0
87
+ self.highlighter = SourceHighlighter(self)
88
+ self.number_margin = SourceNumberMargin(self)
89
+ self.updateRequest.connect(lambda rectangle, distance: self.number_margin.update())
90
+ self.blockCountChanged.connect(self.refresh_metrics)
91
+ self.refresh_metrics()
92
+
93
+ def set_filename(self, basename: str) -> None:
94
+ """Choose a lexer by historical filename, falling back to literal plain text."""
95
+ try:
96
+ self.highlighter.lexer = get_lexer_for_filename(basename)
97
+ except ClassNotFound:
98
+ self.highlighter.lexer = None
99
+ self.highlighter.cached_by_line_index.clear()
100
+ self.highlighter.rehighlight()
101
+
102
+ def clear(self) -> None:
103
+ """Discard stale syntax cache and numbering when changing historical files."""
104
+ self.syntax_generation += 1
105
+ self.highlighter.cached_by_line_index.clear()
106
+ super().clear()
107
+ self.refresh_metrics()
108
+
109
+ def refresh_syntax(self) -> None:
110
+ """Finish syntax for split insertion batches while yielding and honoring cancellation."""
111
+ self.syntax_generation += 1
112
+ generation = self.syntax_generation
113
+ block = self.document().begin()
114
+
115
+ def format_blocks() -> None:
116
+ """Revisit only the current file, under the same budget as text insertion."""
117
+ nonlocal block
118
+ if generation != self.syntax_generation:
119
+ return
120
+ deadline = monotonic() + FILE_PARSE_BUDGET_MS / 1000
121
+ while block.isValid() and monotonic() < deadline:
122
+ self.highlighter.rehighlightBlock(block)
123
+ block = block.next()
124
+ if block.isValid():
125
+ QTimer.singleShot(0, format_blocks)
126
+
127
+ format_blocks()
128
+
129
+ def refresh_metrics(self) -> None:
130
+ """Keep numbers aligned with the current font, palette and source line count."""
131
+ width = self.fontMetrics().horizontalAdvance(str(len(self.line_positions))) + LAYOUT_SPACING * 2
132
+ self.setViewportMargins(width, 0, 0, 0)
133
+ self.number_margin.setGeometry(self.contentsRect().left(), self.viewport().geometry().top(), width, self.viewport().height())
134
+ self.number_margin.update()
135
+ dark = self.palette().color(QPalette.ColorRole.Base).lightnessF() < 0.5
136
+ style = get_style_by_name("native" if dark else "default")
137
+ if style != self.syntax_style:
138
+ self.syntax_style = style
139
+ self.highlighter.cached_by_line_index.clear()
140
+ self.highlighter.rehighlight()
141
+
142
+ def resizeEvent(self, event: QResizeEvent) -> None:
143
+ """Keep the margin aligned when a splitter changes the reader size."""
144
+ super().resizeEvent(event)
145
+ self.refresh_metrics()
146
+
147
+ def changeEvent(self, event: QEvent) -> None:
148
+ """Refresh source decoration after native font or palette changes."""
149
+ super().changeEvent(event)
150
+ if event.type() in (QEvent.Type.FontChange, QEvent.Type.PaletteChange) and hasattr(self, "number_margin"):
151
+ self.refresh_metrics()
@@ -0,0 +1,67 @@
1
+ """Turn lexer tokens into Qt text format ranges, within a length bound, without generating HTML."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bisect import bisect_right
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtGui import QColor, QTextCharFormat
10
+
11
+ from gitpane.config import DIFF_SYNTAX_LIMIT_CHARS
12
+ from gitpane.history.revision_file import utf16_length
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
17
+ from pygments.lexer import Lexer
18
+ from pygments.style import Style
19
+
20
+
21
+ @dataclass
22
+ class SyntaxLine:
23
+ """Index token ends so continuation blocks visit only their own syntax ranges."""
24
+
25
+ spans: list[tuple[int, int, QTextCharFormat]]
26
+ end_positions: list[int]
27
+
28
+ def block_spans(self, start: int, length: int) -> Iterator[tuple[int, int, QTextCharFormat]]:
29
+ """Yield clipped ranges using a binary search instead of rescanning the line."""
30
+ index = bisect_right(self.end_positions, start)
31
+ end = start + length
32
+ while index < len(self.spans):
33
+ offset, token_length, token_format = self.spans[index]
34
+ if offset >= end:
35
+ break
36
+ clipped_start = max(start, offset)
37
+ clipped_end = min(end, offset + token_length)
38
+ if clipped_end > clipped_start:
39
+ yield clipped_start - start, clipped_end - clipped_start, token_format
40
+ index += 1
41
+
42
+
43
+ def syntax_spans(text: str, lexer: Lexer, style: type[Style], prefix_length: int = 0) -> SyntaxLine:
44
+ """Convert lexer tokens into literal UTF-16 format ranges without generating HTML."""
45
+ spans = []
46
+ end_positions = []
47
+ if len(text) > DIFF_SYNTAX_LIMIT_CHARS:
48
+ return SyntaxLine(spans, end_positions)
49
+ previous = prefix_length
50
+ position = utf16_length(text[:prefix_length])
51
+ for offset, token, value in lexer.get_tokens_unprocessed(text[prefix_length:]):
52
+ start = offset + prefix_length
53
+ position += utf16_length(text[previous:start])
54
+ length = utf16_length(value)
55
+ # Lexers can emit specialized tokens absent from a theme; inherit their nearest styled parent.
56
+ while not style.styles_token(token) and token.parent is not None:
57
+ token = token.parent
58
+ token_style = style.style_for_token(token)
59
+ token_format = QTextCharFormat()
60
+ if token_style["color"]:
61
+ token_format.setForeground(QColor("#" + token_style["color"]))
62
+ token_format.setFontItalic(token_style["italic"])
63
+ spans.append((position, length, token_format))
64
+ position += length
65
+ end_positions.append(position)
66
+ previous = start + len(value)
67
+ return SyntaxLine(spans, end_positions)
File without changes
@@ -0,0 +1,106 @@
1
+ """Offer the branches of a reference snapshot in a drop-down list, the checked-out one in bold."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QSignalBlocker, Qt
8
+ from PySide6.QtWidgets import QComboBox, QStyledItemDelegate
9
+
10
+ from gitpane.config import SHORT_OID_LENGTH
11
+ from gitpane.history.repository_value import ReferenceKind
12
+ from gitpane.ui.look.reference_font import reference_font
13
+
14
+ if TYPE_CHECKING:
15
+ from PySide6.QtGui import QWheelEvent
16
+ from PySide6.QtWidgets import QWidget
17
+
18
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
19
+
20
+ BRANCH_VISIBLE_ROWS = 20
21
+ BRANCH_MINIMUM_CHARACTERS = 18
22
+
23
+
24
+ class BranchSelector(QComboBox):
25
+ """Keep full references in item data and grouped headers out of keyboard selection."""
26
+
27
+ def __init__(self, parent: QWidget | None = None) -> None:
28
+ """Bound toolbar width independently of branch name length and collection size."""
29
+ super().__init__(parent)
30
+ self.snapshot: ReferenceSnapshot | None = None
31
+ self.setItemDelegate(QStyledItemDelegate(self))
32
+ self.setLabelDrawingMode(QComboBox.LabelDrawingMode.UseDelegate)
33
+ self.setAccessibleName("Select branch")
34
+ self.setMinimumContentsLength(BRANCH_MINIMUM_CHARACTERS)
35
+ self.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
36
+ self.setMaxVisibleItems(BRANCH_VISIBLE_ROWS)
37
+ self.setToolTip("Loading references…")
38
+ self.show_loading()
39
+ self.currentIndexChanged.connect(lambda: self.setToolTip(self.currentData() or self.currentText()))
40
+
41
+ def wheelEvent(self, event: QWheelEvent) -> None:
42
+ """Let the surrounding form scroll without changing a branch accidentally."""
43
+ event.ignore()
44
+
45
+ def _heading(self, text: str) -> None:
46
+ self.addItem(text)
47
+ item = self.model().item(self.count() - 1)
48
+ item.setFlags(Qt.ItemFlag.NoItemFlags)
49
+
50
+ def show_loading(self) -> None:
51
+ """Retain the real current label while disabling incomplete choices."""
52
+ if not self.count():
53
+ self._heading("Loading references…")
54
+ self.setEnabled(False)
55
+ self.setToolTip("Loading references…")
56
+
57
+ def forget_references(self) -> None:
58
+ """Drop the branches of the repository shown before and wait, disabled, for the references of the new one.
59
+
60
+ Unlike show_loading, the previous current label is not kept: it named a branch of another repository, and it
61
+ would stay on screen for good if the references of the new repository could not be read.
62
+ """
63
+ self.snapshot = None
64
+ with QSignalBlocker(self):
65
+ self.clear()
66
+ self.setAccessibleDescription("")
67
+ self.show_loading()
68
+
69
+ def show_error(self, cause: str) -> None:
70
+ """Keep the last known HEAD visible and explain why references cannot be used."""
71
+ self.setEnabled(False)
72
+ self.setToolTip(cause)
73
+ self.setAccessibleDescription(cause)
74
+
75
+ def populate(self, snapshot: ReferenceSnapshot, selected_ref: str | None = None) -> None:
76
+ """Group real local and remote branches while retaining exact item identities."""
77
+ self.snapshot = snapshot
78
+ with QSignalBlocker(self):
79
+ self.clear()
80
+ if snapshot.head_ref is None:
81
+ label = "Detached HEAD — " + snapshot.head_oid[:SHORT_OID_LENGTH] if snapshot.head_oid else "No HEAD commit"
82
+ self._heading(label)
83
+ self.setItemData(self.count() - 1, snapshot.head_oid, Qt.ItemDataRole.ToolTipRole)
84
+ for kind, empty in ((ReferenceKind.HEADS, "No local branches"), (ReferenceKind.REMOTES, "No remote branches")):
85
+ self._heading(kind.label)
86
+ count = 0
87
+ for reference in sorted(snapshot.references, key=lambda reference: reference.full_name):
88
+ if reference.kind != kind:
89
+ continue
90
+ label = reference.full_name.removeprefix(kind.prefix)
91
+ if reference.symbolic_target:
92
+ self._heading(label + " → " + reference.symbolic_target)
93
+ continue
94
+ if reference.target_oid is None:
95
+ self._heading(label + " (commit unavailable)")
96
+ continue
97
+ self.addItem(label, reference.full_name)
98
+ self.setItemData(self.count() - 1, reference_font(self.font(), reference), Qt.ItemDataRole.FontRole)
99
+ self.setItemData(self.count() - 1, reference.full_name, Qt.ItemDataRole.ToolTipRole)
100
+ count += 1
101
+ if not count:
102
+ self._heading(empty)
103
+ index = self.findData(selected_ref or snapshot.head_ref) if selected_ref or snapshot.head_ref else 0
104
+ self.setCurrentIndex(index)
105
+ self.setEnabled(True)
106
+ self.setToolTip(self.currentData() or self.currentText())