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.
- gitpane/__init__.py +0 -0
- gitpane/account/__init__.py +0 -0
- gitpane/account/account_choice.py +297 -0
- gitpane/account/address_owner.py +67 -0
- gitpane/account/authentication.py +1811 -0
- gitpane/account/authentication_worker.py +836 -0
- gitpane/account/credential_manager.py +480 -0
- gitpane/account/credential_manager_install.py +171 -0
- gitpane/account/keyring_reader.py +135 -0
- gitpane/account/keyring_state.py +158 -0
- gitpane/account/ssh.py +312 -0
- gitpane/account/ssh_askpass.py +216 -0
- gitpane/account/ssh_prompt.py +222 -0
- gitpane/application.py +684 -0
- gitpane/commit/__init__.py +0 -0
- gitpane/commit/commit_metadata.py +88 -0
- gitpane/commit/commit_push.py +276 -0
- gitpane/commit/commit_write.py +830 -0
- gitpane/config.py +1066 -0
- gitpane/controller/__init__.py +0 -0
- gitpane/controller/account_controller.py +677 -0
- gitpane/controller/clone_controller.py +776 -0
- gitpane/controller/close_readiness.py +212 -0
- gitpane/controller/commit_timing_controller.py +566 -0
- gitpane/controller/conflict_controller.py +885 -0
- gitpane/controller/console_registry.py +38 -0
- gitpane/controller/emoji_window_controller.py +95 -0
- gitpane/controller/file_action_controller.py +479 -0
- gitpane/controller/history_read_controller.py +671 -0
- gitpane/controller/history_rewrite_controller.py +739 -0
- gitpane/controller/navigation_controller.py +658 -0
- gitpane/controller/opening_controller.py +375 -0
- gitpane/controller/operation_policy.py +76 -0
- gitpane/controller/rebase_controller.py +797 -0
- gitpane/controller/rebase_editor_controller.py +309 -0
- gitpane/controller/reference_form_controller.py +1677 -0
- gitpane/controller/reference_read_controller.py +242 -0
- gitpane/controller/refresh_controller.py +477 -0
- gitpane/controller/replay_controller.py +805 -0
- gitpane/controller/session_registry.py +337 -0
- gitpane/controller/setting_controller.py +592 -0
- gitpane/controller/shutdown_controller.py +345 -0
- gitpane/controller/staging_controller.py +199 -0
- gitpane/controller/staging_session_registry.py +47 -0
- gitpane/controller/stash_controller.py +367 -0
- gitpane/controller/transport_controller.py +813 -0
- gitpane/controller/welcome_controller.py +102 -0
- gitpane/controller/window_activation.py +116 -0
- gitpane/controller/worktree_state_controller.py +169 -0
- gitpane/emoji/__init__.py +0 -0
- gitpane/emoji/emoji_catalogue.py +156 -0
- gitpane/git_config/__init__.py +0 -0
- gitpane/git_config/git_setting.py +441 -0
- gitpane/history/__init__.py +0 -0
- gitpane/history/avatar.py +241 -0
- gitpane/history/file_history.py +359 -0
- gitpane/history/graph.py +145 -0
- gitpane/history/history_filter.py +31 -0
- gitpane/history/history_parse.py +605 -0
- gitpane/history/repository.py +1600 -0
- gitpane/history/repository_value.py +341 -0
- gitpane/history/revision.py +180 -0
- gitpane/history/revision_file.py +182 -0
- gitpane/history_rewrite/__init__.py +0 -0
- gitpane/history_rewrite/graph.py +210 -0
- gitpane/history_rewrite/guard.py +288 -0
- gitpane/history_rewrite/index.py +125 -0
- gitpane/history_rewrite/prepare.py +378 -0
- gitpane/history_rewrite/protocol.py +320 -0
- gitpane/history_rewrite/publish.py +444 -0
- gitpane/history_rewrite/recovery.py +481 -0
- gitpane/history_rewrite/value.py +229 -0
- gitpane/operation/__init__.py +0 -0
- gitpane/operation/console_line.py +313 -0
- gitpane/operation/console_tone.py +76 -0
- gitpane/operation/diagnostic.py +954 -0
- gitpane/operation/launch_signature.py +254 -0
- gitpane/operation/operation_status.py +87 -0
- gitpane/operation/operation_trace.py +766 -0
- gitpane/operation/repository_operation.py +781 -0
- gitpane/preference/__init__.py +0 -0
- gitpane/preference/preference_location.py +40 -0
- gitpane/preference/preference_store.py +590 -0
- gitpane/process/__init__.py +0 -0
- gitpane/process/exact_arguments.py +77 -0
- gitpane/process/git_output_text.py +20 -0
- gitpane/process/process_group.py +851 -0
- gitpane/process/process_log.py +114 -0
- gitpane/process/process_runner.py +1033 -0
- gitpane/process/released_child_watch.py +138 -0
- gitpane/recent/__init__.py +0 -0
- gitpane/recent/mount_table.py +422 -0
- gitpane/recent/recent_availability.py +414 -0
- gitpane/recent/recent_name.py +11 -0
- gitpane/recent/recent_search.py +114 -0
- gitpane/reference/__init__.py +0 -0
- gitpane/reference/reference_edit.py +2030 -0
- gitpane/reference/reference_snapshot.py +196 -0
- gitpane/reference/stash.py +730 -0
- gitpane/reference/stash_list.py +164 -0
- gitpane/sequencer/__init__.py +0 -0
- gitpane/sequencer/commit_message_rule.py +154 -0
- gitpane/sequencer/conflict.py +506 -0
- gitpane/sequencer/merge_recovery.py +163 -0
- gitpane/sequencer/merge_state.py +166 -0
- gitpane/sequencer/mergetool.py +856 -0
- gitpane/sequencer/rebase.py +750 -0
- gitpane/sequencer/rebase_edit_file.py +224 -0
- gitpane/sequencer/rebase_state.py +523 -0
- gitpane/sequencer/replay.py +1435 -0
- gitpane/sequencer/replay_legacy.py +253 -0
- gitpane/sequencer/replay_state.py +747 -0
- gitpane/submodule/__init__.py +0 -0
- gitpane/submodule/checkout_refusal.py +102 -0
- gitpane/submodule/checkout_refusal_notice.py +111 -0
- gitpane/submodule/cloned_without_checkout.py +65 -0
- gitpane/submodule/submodule_discovery.py +1180 -0
- gitpane/submodule/submodule_loss.py +218 -0
- gitpane/system/__init__.py +0 -0
- gitpane/system/appearance_portal.py +125 -0
- gitpane/system/application_identity.py +48 -0
- gitpane/system/command_line.py +36 -0
- gitpane/system/desktop_icon_fact.py +128 -0
- gitpane/system/desktop_icon_support.py +270 -0
- gitpane/system/desktop_shortcut.py +536 -0
- gitpane/system/icns_image.py +35 -0
- gitpane/system/installed_launcher.py +136 -0
- gitpane/system/linux_distribution.py +151 -0
- gitpane/system/network_folder.py +46 -0
- gitpane/system/path_identity.py +95 -0
- gitpane/system/private_tree_removal.py +47 -0
- gitpane/system/process_program.py +63 -0
- gitpane/system/qt_folder.py +38 -0
- gitpane/system/ready_channel_watch.py +67 -0
- gitpane/system/startup_activation.py +65 -0
- gitpane/system/terminal_detach.py +384 -0
- gitpane/system/xdg_session.py +85 -0
- gitpane/text/__init__.py +0 -0
- gitpane/text/text_query.py +97 -0
- gitpane/text/typed_path.py +75 -0
- gitpane/text/typed_text.py +17 -0
- gitpane/thread/__init__.py +0 -0
- gitpane/thread/weak_signal.py +93 -0
- gitpane/tool/__init__.py +0 -0
- gitpane/tool/app_bundle.py +164 -0
- gitpane/tool/desktop_text_editor.py +156 -0
- gitpane/tool/difftool.py +429 -0
- gitpane/tool/editor.py +445 -0
- gitpane/tool/editor_bridge.py +193 -0
- gitpane/tool/editor_recipe.py +481 -0
- gitpane/tool/file_reveal.py +94 -0
- gitpane/tool/git_executable.py +264 -0
- gitpane/tool/git_version.py +62 -0
- gitpane/tool/kdiff3.py +41 -0
- gitpane/tool/kdiff3_install.py +162 -0
- gitpane/tool/launch_services_text_editor.py +104 -0
- gitpane/tool/mergetool_recipe.py +84 -0
- gitpane/tool/system_editor_default.py +80 -0
- gitpane/tool/system_text_editor.py +44 -0
- gitpane/tool/system_tool.py +816 -0
- gitpane/tool/terminal_recipe.py +390 -0
- gitpane/transport/__init__.py +0 -0
- gitpane/transport/branch_exchange.py +1690 -0
- gitpane/transport/clone.py +1273 -0
- gitpane/transport/credential_read_retry.py +35 -0
- gitpane/transport/git_refusal.py +207 -0
- gitpane/transport/push_tracking.py +121 -0
- gitpane/transport/refusal_notice.py +225 -0
- gitpane/transport/remote.py +747 -0
- gitpane/transport/remote_config.py +544 -0
- gitpane/ui/__init__.py +0 -0
- gitpane/ui/console/__init__.py +0 -0
- gitpane/ui/console/error_dialog.py +332 -0
- gitpane/ui/console/operation_dialog.py +408 -0
- gitpane/ui/console/operation_output.py +91 -0
- gitpane/ui/console/operation_output_view.py +373 -0
- gitpane/ui/emoji/__init__.py +0 -0
- gitpane/ui/emoji/emoji_font.py +257 -0
- gitpane/ui/emoji/emoji_grid.py +125 -0
- gitpane/ui/emoji/emoji_window.py +154 -0
- gitpane/ui/emoji/file_action_icon.py +131 -0
- gitpane/ui/emoji/opentype_family_name.py +182 -0
- gitpane/ui/history_rewrite/__init__.py +0 -0
- gitpane/ui/history_rewrite/details.py +78 -0
- gitpane/ui/history_rewrite/preview.py +565 -0
- gitpane/ui/history_rewrite/preview_model.py +291 -0
- gitpane/ui/history_rewrite/question.py +87 -0
- gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
- gitpane/ui/look/__init__.py +0 -0
- gitpane/ui/look/action_icon.py +504 -0
- gitpane/ui/look/application_icon.py +78 -0
- gitpane/ui/look/application_style.py +317 -0
- gitpane/ui/look/avatar_label.py +53 -0
- gitpane/ui/look/color_contrast.py +43 -0
- gitpane/ui/look/desktop_appearance_follower.py +118 -0
- gitpane/ui/look/fixed_font.py +21 -0
- gitpane/ui/look/gtk_settings.py +116 -0
- gitpane/ui/look/icon_density.py +16 -0
- gitpane/ui/look/reference_font.py +19 -0
- gitpane/ui/look/theme_color.py +63 -0
- gitpane/ui/look/theme_look.py +95 -0
- gitpane/ui/look/theme_palette.py +173 -0
- gitpane/ui/look/warning_icon.py +86 -0
- gitpane/ui/look/windows_control_look.py +250 -0
- gitpane/ui/reader/__init__.py +0 -0
- gitpane/ui/reader/commit_message.py +129 -0
- gitpane/ui/reader/commit_panel.py +378 -0
- gitpane/ui/reader/diff_panel.py +626 -0
- gitpane/ui/reader/file_action.py +138 -0
- gitpane/ui/reader/file_tree_panel.py +235 -0
- gitpane/ui/reader/reader_toolbar.py +56 -0
- gitpane/ui/reader/revision_tree.py +80 -0
- gitpane/ui/reader/search_result.py +165 -0
- gitpane/ui/reader/source_text.py +134 -0
- gitpane/ui/reader/source_view.py +151 -0
- gitpane/ui/reader/syntax_span.py +67 -0
- gitpane/ui/reference/__init__.py +0 -0
- gitpane/ui/reference/branch_selector.py +106 -0
- gitpane/ui/reference/checkout_dialog.py +419 -0
- gitpane/ui/reference/checkout_revision_dialog.py +182 -0
- gitpane/ui/reference/commit_summary.py +339 -0
- gitpane/ui/reference/create_branch_dialog.py +346 -0
- gitpane/ui/reference/create_tag_dialog.py +283 -0
- gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
- gitpane/ui/reference/delete_tag_dialog.py +394 -0
- gitpane/ui/reference/form_opening.py +53 -0
- gitpane/ui/reference/reference_dialog.py +417 -0
- gitpane/ui/reference/reference_feedback.py +62 -0
- gitpane/ui/reference/reference_form.py +139 -0
- gitpane/ui/reference/rename_branch_dialog.py +179 -0
- gitpane/ui/reference/reset_branch_dialog.py +430 -0
- gitpane/ui/reference/reset_dialog.py +263 -0
- gitpane/ui/reference/revision_dialog.py +121 -0
- gitpane/ui/reference/revision_picker.py +227 -0
- gitpane/ui/reference/select_branches_dialog.py +199 -0
- gitpane/ui/reference/stash_dialog.py +171 -0
- gitpane/ui/repository/__init__.py +0 -0
- gitpane/ui/repository/graph_delegate.py +298 -0
- gitpane/ui/repository/history_avatar.py +130 -0
- gitpane/ui/repository/history_model.py +179 -0
- gitpane/ui/repository/repository_list_popup.py +319 -0
- gitpane/ui/repository/repository_window.py +2580 -0
- gitpane/ui/resource/ArrowDown.png +0 -0
- gitpane/ui/resource/ArrowUp.png +0 -0
- gitpane/ui/resource/BranchCheckout.png +0 -0
- gitpane/ui/resource/BranchCreate.png +0 -0
- gitpane/ui/resource/BranchDelete.png +0 -0
- gitpane/ui/resource/CopyToClipboard.png +0 -0
- gitpane/ui/resource/GitPane-16.png +0 -0
- gitpane/ui/resource/GitPane-24.png +0 -0
- gitpane/ui/resource/GitPane-256.png +0 -0
- gitpane/ui/resource/GitPane-32.png +0 -0
- gitpane/ui/resource/GitPane-48.png +0 -0
- gitpane/ui/resource/GitPane-64.png +0 -0
- gitpane/ui/resource/GitPane.ico +0 -0
- gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
- gitpane/ui/resource/Renamed.png +0 -0
- gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
- gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
- gitpane/ui/resource/Stash.png +0 -0
- gitpane/ui/resource/Tag.png +0 -0
- gitpane/ui/resource/TagCreate.png +0 -0
- gitpane/ui/resource/TagDelete.png +0 -0
- gitpane/ui/resource/Unmerged.png +0 -0
- gitpane/ui/resource/checkout.png +0 -0
- gitpane/ui/sequencer/__init__.py +0 -0
- gitpane/ui/sequencer/conflict_dialog.py +532 -0
- gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
- gitpane/ui/sequencer/rebase_dialog.py +461 -0
- gitpane/ui/sequencer/rebase_editor_window.py +220 -0
- gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
- gitpane/ui/sequencer/replay_dialog.py +501 -0
- gitpane/ui/setting/__init__.py +0 -0
- gitpane/ui/setting/account_page.py +724 -0
- gitpane/ui/setting/add_account_dialog.py +1005 -0
- gitpane/ui/setting/credential_manager_dialog.py +151 -0
- gitpane/ui/setting/git_setting_widget.py +388 -0
- gitpane/ui/setting/setting_block.py +19 -0
- gitpane/ui/setting/setting_dialog.py +750 -0
- gitpane/ui/setting/ssh_widget.py +232 -0
- gitpane/ui/setting/utility_page.py +240 -0
- gitpane/ui/staging/__init__.py +0 -0
- gitpane/ui/staging/staging_action.py +356 -0
- gitpane/ui/staging/staging_window.py +745 -0
- gitpane/ui/transport/__init__.py +0 -0
- gitpane/ui/transport/clone_dialog.py +879 -0
- gitpane/ui/transport/push_dialog.py +580 -0
- gitpane/ui/transport/push_rejection_prompt.py +100 -0
- gitpane/ui/transport/push_tracking_question.py +88 -0
- gitpane/ui/transport/remote_dialog.py +943 -0
- gitpane/ui/welcome/__init__.py +0 -0
- gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
- gitpane/ui/welcome/welcome_window.py +1137 -0
- gitpane/ui/widget/__init__.py +0 -0
- gitpane/ui/widget/busy_indicator.py +106 -0
- gitpane/ui/widget/commit_date_text.py +55 -0
- gitpane/ui/widget/content_growth.py +262 -0
- gitpane/ui/widget/content_measure.py +99 -0
- gitpane/ui/widget/dialog_layout.py +668 -0
- gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
- gitpane/ui/widget/flow_layout.py +110 -0
- gitpane/ui/widget/long_text.py +542 -0
- gitpane/ui/widget/mnemonic.py +13 -0
- gitpane/ui/widget/numbered_text_edit.py +127 -0
- gitpane/ui/widget/plain_label.py +15 -0
- gitpane/ui/widget/plain_tooltip.py +19 -0
- gitpane/ui/widget/refresh_advice.py +25 -0
- gitpane/ui/widget/result_label.py +227 -0
- gitpane/ui/widget/result_status_bar.py +53 -0
- gitpane/ui/widget/scrolled_text.py +69 -0
- gitpane/ui/widget/shortcut_text.py +31 -0
- gitpane/ui/widget/standard_icon_label.py +55 -0
- gitpane/ui/widget/system_shortcut.py +75 -0
- gitpane/ui/widget/unavailable_reason.py +29 -0
- gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
- gitpane/ui/window_control/__init__.py +0 -0
- gitpane/ui/window_control/button_focus.py +42 -0
- gitpane/ui/window_control/compositor_answer.py +152 -0
- gitpane/ui/window_control/compositor_growth_cap.py +73 -0
- gitpane/ui/window_control/desktop_activation.py +203 -0
- gitpane/ui/window_control/dialog_modality.py +122 -0
- gitpane/ui/window_control/item_activation.py +42 -0
- gitpane/ui/window_control/modal_focus_relay.py +124 -0
- gitpane/ui/window_control/wayland_activation.py +695 -0
- gitpane/ui/window_control/window_placement.py +305 -0
- gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane/vendor/w32.exe +0 -0
- gitpane/vendor/w64-arm.exe +0 -0
- gitpane/vendor/w64.exe +0 -0
- gitpane/worktree/__init__.py +0 -0
- gitpane/worktree/command_sequence.py +110 -0
- gitpane/worktree/file_action_target.py +61 -0
- gitpane/worktree/file_menu_action.py +52 -0
- gitpane/worktree/historical_restore.py +361 -0
- gitpane/worktree/ignore_rule.py +266 -0
- gitpane/worktree/mutation.py +1539 -0
- gitpane/worktree/partial_patch.py +435 -0
- gitpane/worktree/staged_reset.py +447 -0
- gitpane/worktree/staging.py +1418 -0
- gitpane/worktree/trash.py +302 -0
- gitpane/worktree/worktree_file.py +406 -0
- gitpane/worktree/worktree_service.py +584 -0
- gitpane/worktree/worktree_snapshot.py +630 -0
- gitpane-1.0.0.dist-info/METADATA +141 -0
- gitpane-1.0.0.dist-info/RECORD +353 -0
- gitpane-1.0.0.dist-info/WHEEL +5 -0
- gitpane-1.0.0.dist-info/entry_points.txt +2 -0
- gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- 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())
|