je-editor 0.0.202__py3-none-any.whl → 0.0.228__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.
- je_editor/__init__.py +2 -2
- je_editor/code_scan/__init__.py +0 -0
- je_editor/code_scan/ruff_thread.py +58 -0
- je_editor/code_scan/watchdog_implement.py +56 -0
- je_editor/code_scan/watchdog_thread.py +78 -0
- je_editor/git_client/__init__.py +0 -0
- je_editor/git_client/commit_graph.py +77 -0
- je_editor/git_client/git_action.py +175 -0
- je_editor/git_client/git_cli.py +66 -0
- je_editor/pyside_ui/browser/browser_download_window.py +40 -4
- je_editor/pyside_ui/browser/browser_serach_lineedit.py +24 -0
- je_editor/pyside_ui/browser/browser_view.py +55 -6
- je_editor/pyside_ui/browser/browser_widget.py +62 -19
- je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
- je_editor/pyside_ui/code/auto_save/auto_save_manager.py +33 -1
- je_editor/pyside_ui/code/auto_save/auto_save_thread.py +18 -5
- je_editor/pyside_ui/code/code_format/pep8_format.py +52 -7
- je_editor/pyside_ui/code/code_process/code_exec.py +118 -64
- je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +115 -53
- je_editor/pyside_ui/code/running_process_manager.py +18 -0
- je_editor/pyside_ui/code/shell_process/shell_exec.py +99 -60
- je_editor/pyside_ui/code/syntax/python_syntax.py +44 -9
- je_editor/pyside_ui/code/syntax/syntax_setting.py +39 -11
- je_editor/pyside_ui/code/textedit_code_result/code_record.py +33 -11
- je_editor/pyside_ui/code/variable_inspector/__init__.py +0 -0
- je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +172 -0
- je_editor/pyside_ui/dialog/ai_dialog/__init__.py +0 -0
- je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +71 -0
- je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +38 -4
- je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +32 -4
- je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +25 -2
- je_editor/pyside_ui/dialog/search_ui/search_error_box.py +27 -2
- je_editor/pyside_ui/dialog/search_ui/search_text_box.py +28 -3
- je_editor/pyside_ui/git_ui/__init__.py +0 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/__init__.py +0 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +90 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py +141 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py +88 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py +284 -0
- je_editor/pyside_ui/git_ui/git_client/__init__.py +0 -0
- je_editor/pyside_ui/git_ui/git_client/commit_table.py +65 -0
- je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +156 -0
- je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +799 -0
- je_editor/pyside_ui/git_ui/git_client/graph_view.py +218 -0
- je_editor/pyside_ui/main_ui/ai_widget/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +34 -0
- je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +36 -0
- je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +147 -0
- je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +84 -0
- je_editor/pyside_ui/main_ui/console_widget/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/console_widget/console_gui.py +162 -0
- je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +84 -0
- je_editor/pyside_ui/main_ui/dock/destroy_dock.py +32 -1
- je_editor/pyside_ui/main_ui/editor/editor_widget.py +113 -23
- je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +33 -6
- je_editor/pyside_ui/main_ui/editor/process_input.py +42 -10
- je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +45 -10
- je_editor/pyside_ui/main_ui/main_editor.py +189 -49
- je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +50 -27
- je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +141 -30
- je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +67 -17
- je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +33 -3
- je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +39 -0
- je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +102 -40
- je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +53 -6
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +47 -3
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +45 -5
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +41 -3
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +21 -0
- je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +37 -11
- je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +42 -6
- je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +202 -27
- je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +66 -0
- je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +18 -1
- je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +32 -2
- je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +37 -10
- je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +39 -0
- je_editor/start_editor.py +25 -0
- je_editor/utils/encodings/python_encodings.py +100 -97
- je_editor/utils/exception/exception_tags.py +11 -11
- je_editor/utils/file/open/open_file.py +35 -19
- je_editor/utils/file/save/save_file.py +34 -13
- je_editor/utils/json/json_file.py +29 -14
- je_editor/utils/json_format/json_process.py +32 -1
- je_editor/utils/logging/loggin_instance.py +37 -7
- je_editor/utils/multi_language/english.py +102 -6
- je_editor/utils/multi_language/multi_language_wrapper.py +28 -3
- je_editor/utils/multi_language/traditional_chinese.py +101 -10
- je_editor/utils/redirect_manager/redirect_manager_class.py +49 -11
- je_editor/utils/venv_check/check_venv.py +37 -14
- {je_editor-0.0.202.dist-info → je_editor-0.0.228.dist-info}/METADATA +12 -6
- je_editor-0.0.228.dist-info/RECORD +140 -0
- {je_editor-0.0.202.dist-info → je_editor-0.0.228.dist-info}/WHEEL +1 -1
- je_editor-0.0.202.dist-info/RECORD +0 -108
- {je_editor-0.0.202.dist-info → je_editor-0.0.228.dist-info/licenses}/LICENSE +0 -0
- {je_editor-0.0.202.dist-info → je_editor-0.0.228.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from PySide6.QtCore import QRect, QSize, Qt
|
|
2
|
+
from PySide6.QtGui import QPainter, QColor
|
|
3
|
+
from PySide6.QtWidgets import QPlainTextEdit, QWidget, QTextEdit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LineNumberArea(QWidget):
|
|
7
|
+
"""
|
|
8
|
+
Side widget for displaying line numbers.
|
|
9
|
+
用來顯示行號的側邊元件。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, editor):
|
|
13
|
+
super().__init__(editor)
|
|
14
|
+
self.code_editor = editor # 綁定主要的編輯器 / Bind to main editor
|
|
15
|
+
|
|
16
|
+
def sizeHint(self):
|
|
17
|
+
"""
|
|
18
|
+
Suggest width for line number area.
|
|
19
|
+
建議行號區域的寬度。
|
|
20
|
+
"""
|
|
21
|
+
return QSize(self.code_editor.line_number_area_width(), 0)
|
|
22
|
+
|
|
23
|
+
def paintEvent(self, event):
|
|
24
|
+
"""
|
|
25
|
+
Delegate paint event to the main editor.
|
|
26
|
+
將繪製事件交給主要編輯器處理。
|
|
27
|
+
"""
|
|
28
|
+
self.code_editor.line_number_area_paint_event(event)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LineNumberedCodeViewer(QPlainTextEdit):
|
|
32
|
+
"""
|
|
33
|
+
QPlainTextEdit with line numbers.
|
|
34
|
+
帶有行號顯示的文字編輯器。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, parent=None):
|
|
38
|
+
super().__init__(parent)
|
|
39
|
+
self.lineNumberArea = LineNumberArea(self)
|
|
40
|
+
|
|
41
|
+
# === 信號連接 / Signal connections ===
|
|
42
|
+
self.blockCountChanged.connect(self.update_line_number_area_width) # 當行數改變時更新行號區寬度
|
|
43
|
+
self.updateRequest.connect(self.update_line_number_area) # 當需要重繪時更新行號區
|
|
44
|
+
self.cursorPositionChanged.connect(self.highlight_current_line) # 當游標移動時高亮當前行
|
|
45
|
+
|
|
46
|
+
# 初始化行號區寬度與高亮
|
|
47
|
+
self.update_line_number_area_width(0)
|
|
48
|
+
self.highlight_current_line()
|
|
49
|
+
|
|
50
|
+
def line_number_area_width(self):
|
|
51
|
+
"""
|
|
52
|
+
Calculate width of line number area based on digit count.
|
|
53
|
+
根據行數位數計算行號區寬度。
|
|
54
|
+
"""
|
|
55
|
+
digits = len(str(max(1, self.blockCount())))
|
|
56
|
+
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
|
|
57
|
+
return space
|
|
58
|
+
|
|
59
|
+
def update_line_number_area_width(self, _):
|
|
60
|
+
"""
|
|
61
|
+
Adjust viewport margins to fit line number area.
|
|
62
|
+
調整視口邊界以容納行號區。
|
|
63
|
+
"""
|
|
64
|
+
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
|
|
65
|
+
|
|
66
|
+
def update_line_number_area(self, rect, dy):
|
|
67
|
+
"""
|
|
68
|
+
Update/redraw line number area when scrolling or editing.
|
|
69
|
+
當滾動或編輯時更新行號區。
|
|
70
|
+
"""
|
|
71
|
+
if dy:
|
|
72
|
+
self.lineNumberArea.scroll(0, dy)
|
|
73
|
+
else:
|
|
74
|
+
self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
|
|
75
|
+
if rect.contains(self.viewport().rect()):
|
|
76
|
+
self.update_line_number_area_width(0)
|
|
77
|
+
|
|
78
|
+
def resizeEvent(self, event):
|
|
79
|
+
"""
|
|
80
|
+
Resize line number area when editor resizes.
|
|
81
|
+
編輯器大小改變時調整行號區。
|
|
82
|
+
"""
|
|
83
|
+
super().resizeEvent(event)
|
|
84
|
+
cr = self.contentsRect()
|
|
85
|
+
self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
|
|
86
|
+
|
|
87
|
+
def line_number_area_paint_event(self, event):
|
|
88
|
+
"""
|
|
89
|
+
Paint line numbers.
|
|
90
|
+
繪製行號。
|
|
91
|
+
"""
|
|
92
|
+
painter = QPainter(self.lineNumberArea)
|
|
93
|
+
# 背景顏色依主題切換 / Background color depends on theme
|
|
94
|
+
painter.fillRect(event.rect(), QColor(40, 40, 40) if getattr(self, "is_dark", False) else QColor(230, 230, 230))
|
|
95
|
+
|
|
96
|
+
block = self.firstVisibleBlock()
|
|
97
|
+
blockNumber = block.blockNumber()
|
|
98
|
+
top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
|
|
99
|
+
bottom = top + int(self.blockBoundingRect(block).height())
|
|
100
|
+
|
|
101
|
+
while block.isValid() and top <= event.rect().bottom():
|
|
102
|
+
if block.isVisible() and bottom >= event.rect().top():
|
|
103
|
+
number = str(blockNumber + 1)
|
|
104
|
+
# 行號顏色依主題切換 / Line number color depends on theme
|
|
105
|
+
painter.setPen(QColor("#d4d4d4") if getattr(self, "is_dark", False) else Qt.GlobalColor.black)
|
|
106
|
+
painter.drawText(
|
|
107
|
+
0, top, self.lineNumberArea.width() - 2, self.fontMetrics().height(),
|
|
108
|
+
Qt.AlignmentFlag.AlignRight, number
|
|
109
|
+
)
|
|
110
|
+
block = block.next()
|
|
111
|
+
top = bottom
|
|
112
|
+
bottom = top + int(self.blockBoundingRect(block).height())
|
|
113
|
+
blockNumber += 1
|
|
114
|
+
|
|
115
|
+
def highlight_current_line(self):
|
|
116
|
+
"""
|
|
117
|
+
Highlight the current line where the cursor is.
|
|
118
|
+
高亮顯示游標所在的行。
|
|
119
|
+
"""
|
|
120
|
+
selection = QTextEdit.ExtraSelection()
|
|
121
|
+
lineColor = QColor(50, 50, 50) if getattr(self, "is_dark", False) else QColor(232, 232, 255)
|
|
122
|
+
selection.format.setBackground(lineColor)
|
|
123
|
+
selection.cursor = self.textCursor()
|
|
124
|
+
selection.cursor.clearSelection()
|
|
125
|
+
|
|
126
|
+
# Keep current-line extras for merging
|
|
127
|
+
self._current_line_extras = [selection]
|
|
128
|
+
|
|
129
|
+
# Merge with diff extras if present
|
|
130
|
+
if hasattr(self, "_diff_extras"):
|
|
131
|
+
merged = self._diff_extras + self._current_line_extras
|
|
132
|
+
else:
|
|
133
|
+
merged = self._current_line_extras
|
|
134
|
+
|
|
135
|
+
self.setExtraSelections(merged)
|
|
136
|
+
|
|
137
|
+
def apply_theme_to_editor(self, dark: bool):
|
|
138
|
+
self.setStyleSheet("QPlainTextEdit { background-color: #1e1e1e; color: #d4d4d4; }" if dark
|
|
139
|
+
else "QPlainTextEdit { background-color: white; color: black; }")
|
|
140
|
+
# Re-trigger current line highlight with the new color
|
|
141
|
+
self.highlight_current_line()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QTabWidget, QVBoxLayout, QWidget
|
|
2
|
+
|
|
3
|
+
from je_editor.pyside_ui.git_ui.code_diff_compare.side_by_side_diff_widget import SideBySideDiffWidget
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MultiFileDiffViewer(QWidget):
|
|
7
|
+
"""
|
|
8
|
+
Multi-file diff viewer using tabs to display each file.
|
|
9
|
+
多檔案差異檢視器,使用分頁 (tab) 來顯示每個檔案的差異。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, parent=None):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the multi-file diff viewer.
|
|
15
|
+
初始化多檔案差異檢視器。
|
|
16
|
+
"""
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
# Tab widget to hold each file's diff
|
|
19
|
+
# 用來存放每個檔案差異的分頁元件
|
|
20
|
+
self.tabs = QTabWidget()
|
|
21
|
+
|
|
22
|
+
# Layout: vertical box with only the tab widget
|
|
23
|
+
# 版面配置:垂直佈局,僅包含分頁元件
|
|
24
|
+
layout = QVBoxLayout(self)
|
|
25
|
+
layout.addWidget(self.tabs)
|
|
26
|
+
|
|
27
|
+
def set_diff_text(self, diff_text: str):
|
|
28
|
+
"""
|
|
29
|
+
Set the diff text and split it into per-file tabs.
|
|
30
|
+
設定差異文字,並依檔案拆分成多個分頁。
|
|
31
|
+
"""
|
|
32
|
+
self.tabs.clear()
|
|
33
|
+
file_diffs = self._split_by_file(diff_text)
|
|
34
|
+
for file_name, ftext in file_diffs:
|
|
35
|
+
viewer = SideBySideDiffWidget() # 每個檔案使用 side-by-side viewer
|
|
36
|
+
viewer.set_diff_text(ftext)
|
|
37
|
+
self.tabs.addTab(viewer, file_name) # 新增分頁,標題為檔名
|
|
38
|
+
|
|
39
|
+
def _split_by_file(self, diff_text: str):
|
|
40
|
+
"""
|
|
41
|
+
Split a unified diff text into chunks per file.
|
|
42
|
+
將 unified diff 文字依檔案切分。
|
|
43
|
+
"""
|
|
44
|
+
chunks = []
|
|
45
|
+
current = []
|
|
46
|
+
file_name = None
|
|
47
|
+
|
|
48
|
+
for line in diff_text.splitlines():
|
|
49
|
+
if line.startswith("diff --git"):
|
|
50
|
+
# 如果遇到新的檔案 diff,先把前一個檔案的內容存起來
|
|
51
|
+
if current and file_name:
|
|
52
|
+
chunks.append((file_name, "\n".join(current)))
|
|
53
|
+
current = []
|
|
54
|
+
# 解析檔名,格式通常為 "diff --git a/file b/file"
|
|
55
|
+
parts = line.split()
|
|
56
|
+
if len(parts) >= 4:
|
|
57
|
+
file_name = parts[2][2:] # 去掉 "a/"
|
|
58
|
+
else:
|
|
59
|
+
file_name = "unknown"
|
|
60
|
+
current.append(line)
|
|
61
|
+
else:
|
|
62
|
+
current.append(line)
|
|
63
|
+
|
|
64
|
+
# 最後一個檔案也要加入結果
|
|
65
|
+
if current and file_name:
|
|
66
|
+
chunks.append((file_name, "\n".join(current)))
|
|
67
|
+
|
|
68
|
+
return chunks
|
|
69
|
+
|
|
70
|
+
def set_dark_theme(self):
|
|
71
|
+
"""
|
|
72
|
+
Apply dark theme to all tabs.
|
|
73
|
+
對所有分頁套用深色主題。
|
|
74
|
+
"""
|
|
75
|
+
for i in range(self.tabs.count()):
|
|
76
|
+
w = self.tabs.widget(i)
|
|
77
|
+
if isinstance(w, SideBySideDiffWidget):
|
|
78
|
+
w.set_dark_theme()
|
|
79
|
+
|
|
80
|
+
def set_light_theme(self):
|
|
81
|
+
"""
|
|
82
|
+
Apply light theme to all tabs.
|
|
83
|
+
對所有分頁套用淺色主題。
|
|
84
|
+
"""
|
|
85
|
+
for i in range(self.tabs.count()):
|
|
86
|
+
w = self.tabs.widget(i)
|
|
87
|
+
if isinstance(w, SideBySideDiffWidget):
|
|
88
|
+
w.set_light_theme()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from PySide6.QtGui import QColor, QTextCursor, QTextCharFormat, QFont
|
|
2
|
+
from PySide6.QtWidgets import QPlainTextEdit, QTextEdit, QWidget, QHBoxLayout, QVBoxLayout, QLabel
|
|
3
|
+
|
|
4
|
+
from je_editor.pyside_ui.git_ui.code_diff_compare.line_number_code_viewer import LineNumberedCodeViewer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SideBySideDiffWidget(QWidget):
|
|
8
|
+
"""
|
|
9
|
+
Side-by-side diff viewer widget.
|
|
10
|
+
左右對照的差異檢視元件。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent=None):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
|
|
16
|
+
# === 顏色設定 / Color configuration ===
|
|
17
|
+
self.is_dark = True
|
|
18
|
+
# 刪除行背景 / Deleted line background
|
|
19
|
+
self.color_del = QColor("#ffcccc") # 淡紅色,深淺背景都清楚
|
|
20
|
+
# 新增行背景 / Added line background
|
|
21
|
+
self.color_add = QColor("#ccffcc") # 淡綠色,對比度佳
|
|
22
|
+
# Hunk header 背景
|
|
23
|
+
self.color_hunk = QColor("#cce5ff") # 淡藍色,醒目但不刺眼
|
|
24
|
+
# Diff header 背景
|
|
25
|
+
self.color_header = QColor("#e0e0e0") # 淺灰,適合標題區塊
|
|
26
|
+
|
|
27
|
+
# === 左右檔名標籤 / File name labels ===
|
|
28
|
+
self.leftLabel = QLabel("Left: (old)")
|
|
29
|
+
self.rightLabel = QLabel("Right: (new)")
|
|
30
|
+
font = QFont()
|
|
31
|
+
font.setBold(True)
|
|
32
|
+
self.leftLabel.setFont(font)
|
|
33
|
+
self.rightLabel.setFont(font)
|
|
34
|
+
|
|
35
|
+
# === 左右文字編輯器 / Left and right code editors ===
|
|
36
|
+
self.leftEdit = LineNumberedCodeViewer()
|
|
37
|
+
self.rightEdit = LineNumberedCodeViewer()
|
|
38
|
+
for edit in (self.leftEdit, self.rightEdit):
|
|
39
|
+
edit.setReadOnly(True) # 設為唯讀 / Read-only
|
|
40
|
+
edit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) # 不自動換行 / Disable line wrap
|
|
41
|
+
mono = QFont("Consolas") # 使用等寬字型 / Monospaced font
|
|
42
|
+
mono.setStyleHint(QFont.StyleHint.Monospace)
|
|
43
|
+
edit.setFont(mono)
|
|
44
|
+
|
|
45
|
+
# === 版面配置 / Layout ===
|
|
46
|
+
leftBox = QVBoxLayout()
|
|
47
|
+
leftBox.addWidget(self.leftLabel)
|
|
48
|
+
leftBox.addWidget(self.leftEdit)
|
|
49
|
+
|
|
50
|
+
rightBox = QVBoxLayout()
|
|
51
|
+
rightBox.addWidget(self.rightLabel)
|
|
52
|
+
rightBox.addWidget(self.rightEdit)
|
|
53
|
+
|
|
54
|
+
main = QHBoxLayout(self)
|
|
55
|
+
leftContainer = QWidget()
|
|
56
|
+
leftContainer.setLayout(leftBox)
|
|
57
|
+
rightContainer = QWidget()
|
|
58
|
+
rightContainer.setLayout(rightBox)
|
|
59
|
+
main.addWidget(leftContainer)
|
|
60
|
+
main.addWidget(rightContainer)
|
|
61
|
+
|
|
62
|
+
# 同步左右捲軸 / Sync scrollbars
|
|
63
|
+
self._sync_scrollbars()
|
|
64
|
+
|
|
65
|
+
# 預設深色模式 / Default to dark theme
|
|
66
|
+
self.set_dark_theme()
|
|
67
|
+
|
|
68
|
+
def _sync_scrollbars(self):
|
|
69
|
+
"""
|
|
70
|
+
Synchronize scrollbars between left and right editors.
|
|
71
|
+
同步左右編輯器的捲軸。
|
|
72
|
+
"""
|
|
73
|
+
self.leftEdit.verticalScrollBar().valueChanged.connect(
|
|
74
|
+
self.rightEdit.verticalScrollBar().setValue
|
|
75
|
+
)
|
|
76
|
+
self.rightEdit.verticalScrollBar().valueChanged.connect(
|
|
77
|
+
self.leftEdit.verticalScrollBar().setValue
|
|
78
|
+
)
|
|
79
|
+
self.leftEdit.horizontalScrollBar().valueChanged.connect(
|
|
80
|
+
self.rightEdit.horizontalScrollBar().setValue
|
|
81
|
+
)
|
|
82
|
+
self.rightEdit.horizontalScrollBar().valueChanged.connect(
|
|
83
|
+
self.leftEdit.horizontalScrollBar().setValue
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def set_diff_text(self, diff_text: str):
|
|
87
|
+
"""
|
|
88
|
+
Parse unified diff text and display it in side-by-side editors.
|
|
89
|
+
解析 unified diff 文字並顯示在左右編輯器。
|
|
90
|
+
"""
|
|
91
|
+
left_lines, right_lines, left_marks, right_marks, left_name, right_name = \
|
|
92
|
+
self._parse_unified_diff(diff_text)
|
|
93
|
+
|
|
94
|
+
self.leftLabel.setText(f"Left: {left_name or '(old)'}")
|
|
95
|
+
self.rightLabel.setText(f"Right: {right_name or '(new)'}")
|
|
96
|
+
|
|
97
|
+
self._set_text_with_highlights(self.leftEdit, left_lines, left_marks)
|
|
98
|
+
self._set_text_with_highlights(self.rightEdit, right_lines, right_marks)
|
|
99
|
+
|
|
100
|
+
# 游標移到開頭 / Move cursor to start
|
|
101
|
+
self.leftEdit.moveCursor(QTextCursor.MoveOperation.Start)
|
|
102
|
+
self.rightEdit.moveCursor(QTextCursor.MoveOperation.Start)
|
|
103
|
+
|
|
104
|
+
def _set_text_with_highlights(self, edit: QPlainTextEdit, lines, marks):
|
|
105
|
+
"""
|
|
106
|
+
Set text and apply syntax highlighting based on diff marks.
|
|
107
|
+
設定文字並依 diff 標記加上背景色。
|
|
108
|
+
"""
|
|
109
|
+
edit.setPlainText("\n".join(lines))
|
|
110
|
+
|
|
111
|
+
diff_extras = []
|
|
112
|
+
for i, mark in enumerate(marks):
|
|
113
|
+
fmt = QTextCharFormat()
|
|
114
|
+
# Always set foreground so it won't fall back
|
|
115
|
+
# 永遠設定前景色,避免 fallback
|
|
116
|
+
fmt.setForeground(QColor("#d4d4d4") if self.is_dark else QColor("black"))
|
|
117
|
+
|
|
118
|
+
if mark == "DEL":
|
|
119
|
+
fmt.setBackground(self.color_del)
|
|
120
|
+
elif mark == "ADD":
|
|
121
|
+
fmt.setBackground(self.color_add)
|
|
122
|
+
elif mark == "HUNK":
|
|
123
|
+
fmt.setBackground(self.color_hunk)
|
|
124
|
+
elif mark == "HDR":
|
|
125
|
+
fmt.setBackground(self.color_header)
|
|
126
|
+
else:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
sel = self._line_selection(edit, i, fmt)
|
|
130
|
+
diff_extras.append(sel)
|
|
131
|
+
|
|
132
|
+
# 保留 diff selections,方便主題切換時重用
|
|
133
|
+
setattr(edit, "_diff_extras", diff_extras)
|
|
134
|
+
|
|
135
|
+
# 嘗試合併其他高亮(例如 LineNumberedCodeViewer 的當前行高亮)
|
|
136
|
+
if hasattr(edit, "_current_line_extras"):
|
|
137
|
+
merged = diff_extras + edit._current_line_extras
|
|
138
|
+
else:
|
|
139
|
+
merged = diff_extras
|
|
140
|
+
|
|
141
|
+
edit.setExtraSelections(merged)
|
|
142
|
+
|
|
143
|
+
def _line_selection(self, edit: QPlainTextEdit, line_index: int, fmt: QTextCharFormat):
|
|
144
|
+
"""
|
|
145
|
+
Create a selection for a specific line with given format.
|
|
146
|
+
建立某一行的選取區並套用格式。
|
|
147
|
+
"""
|
|
148
|
+
sel = QTextEdit.ExtraSelection()
|
|
149
|
+
sel.format = fmt
|
|
150
|
+
cursor = edit.textCursor()
|
|
151
|
+
cursor.movePosition(QTextCursor.MoveOperation.Start)
|
|
152
|
+
for _ in range(line_index):
|
|
153
|
+
cursor.movePosition(QTextCursor.MoveOperation.Down)
|
|
154
|
+
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
|
155
|
+
sel.cursor = cursor
|
|
156
|
+
return sel
|
|
157
|
+
|
|
158
|
+
def _parse_unified_diff(self, diff_text: str):
|
|
159
|
+
"""
|
|
160
|
+
Parse unified diff into left/right lines and marks.
|
|
161
|
+
將 unified diff 解析成左右行與標記。
|
|
162
|
+
"""
|
|
163
|
+
left_lines, right_lines, left_marks, right_marks = [], [], [], []
|
|
164
|
+
left_name, right_name = None, None
|
|
165
|
+
|
|
166
|
+
def add_left(line, mark=None):
|
|
167
|
+
left_lines.append(line)
|
|
168
|
+
left_marks.append(mark or "CTX")
|
|
169
|
+
|
|
170
|
+
def add_right(line, mark=None):
|
|
171
|
+
right_lines.append(line)
|
|
172
|
+
right_marks.append(mark or "CTX")
|
|
173
|
+
|
|
174
|
+
def align():
|
|
175
|
+
# 對齊左右行數 / Align left and right line counts
|
|
176
|
+
if len(left_lines) < len(right_lines):
|
|
177
|
+
for _ in range(len(right_lines) - len(left_lines)):
|
|
178
|
+
add_left("")
|
|
179
|
+
elif len(right_lines) < len(left_lines):
|
|
180
|
+
for _ in range(len(left_lines) - len(right_lines)):
|
|
181
|
+
add_right("")
|
|
182
|
+
|
|
183
|
+
for raw in diff_text.splitlines():
|
|
184
|
+
if raw.startswith("diff "):
|
|
185
|
+
add_left(raw, "HDR");
|
|
186
|
+
add_right(raw, "HDR");
|
|
187
|
+
align()
|
|
188
|
+
elif raw.startswith("--- "):
|
|
189
|
+
left_name = raw[4:].strip()
|
|
190
|
+
add_left(raw, "HDR");
|
|
191
|
+
add_right("", "HDR");
|
|
192
|
+
align()
|
|
193
|
+
elif raw.startswith("+++ "):
|
|
194
|
+
right_name = raw[4:].strip()
|
|
195
|
+
add_left("", "HDR");
|
|
196
|
+
add_right(raw, "HDR");
|
|
197
|
+
align()
|
|
198
|
+
elif raw.startswith("@@"):
|
|
199
|
+
add_left(raw, "HUNK");
|
|
200
|
+
add_right(raw, "HUNK");
|
|
201
|
+
align()
|
|
202
|
+
elif raw.startswith("-"):
|
|
203
|
+
add_left(raw, "DEL");
|
|
204
|
+
add_right("", None);
|
|
205
|
+
align()
|
|
206
|
+
elif raw.startswith("+"):
|
|
207
|
+
add_left("", None);
|
|
208
|
+
add_right(raw, "ADD");
|
|
209
|
+
align()
|
|
210
|
+
else:
|
|
211
|
+
add_left(raw, None);
|
|
212
|
+
add_right(raw, None);
|
|
213
|
+
align()
|
|
214
|
+
|
|
215
|
+
return left_lines, right_lines, left_marks, right_marks, left_name, right_name
|
|
216
|
+
|
|
217
|
+
def _reapply_highlights_for_theme(self):
|
|
218
|
+
"""
|
|
219
|
+
Reapply highlights when theme changes.
|
|
220
|
+
主題切換時重新套用高亮。
|
|
221
|
+
"""
|
|
222
|
+
for edit in (self.leftEdit, self.rightEdit):
|
|
223
|
+
if hasattr(edit, "_diff_extras"):
|
|
224
|
+
updated = []
|
|
225
|
+
for sel in edit._diff_extras:
|
|
226
|
+
fmt: QTextCharFormat = QTextCharFormat(sel.format)
|
|
227
|
+
|
|
228
|
+
# 前景色依主題切換
|
|
229
|
+
fmt.setForeground(QColor("#d4d4d4") if self.is_dark else QColor("black"))
|
|
230
|
+
|
|
231
|
+
# 依照 mark 更新背景色
|
|
232
|
+
cursor = sel.cursor
|
|
233
|
+
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
|
234
|
+
text = cursor.selectedText()
|
|
235
|
+
|
|
236
|
+
if text.startswith("-"):
|
|
237
|
+
fmt.setBackground(self.color_del)
|
|
238
|
+
elif text.startswith("+"):
|
|
239
|
+
fmt.setBackground(self.color_add)
|
|
240
|
+
elif text.startswith("@@"):
|
|
241
|
+
fmt.setBackground(self.color_hunk)
|
|
242
|
+
elif text.startswith("diff") or text.startswith("---") or text.startswith("+++"):
|
|
243
|
+
fmt.setBackground(self.color_header)
|
|
244
|
+
|
|
245
|
+
sel.format = fmt
|
|
246
|
+
updated.append(sel)
|
|
247
|
+
|
|
248
|
+
edit._diff_extras = updated
|
|
249
|
+
|
|
250
|
+
if hasattr(edit, "_current_line_extras"):
|
|
251
|
+
merged = updated + edit._current_line_extras
|
|
252
|
+
else:
|
|
253
|
+
merged = updated
|
|
254
|
+
edit.setExtraSelections(merged)
|
|
255
|
+
|
|
256
|
+
def set_dark_theme(self):
|
|
257
|
+
"""
|
|
258
|
+
Apply dark theme colors.
|
|
259
|
+
套用深色主題配色。
|
|
260
|
+
"""
|
|
261
|
+
self.is_dark = True
|
|
262
|
+
self.color_del = QColor(60, 20, 20)
|
|
263
|
+
self.color_add = QColor(20, 60, 20)
|
|
264
|
+
self.color_hunk = QColor(25, 25, 60)
|
|
265
|
+
self.color_header = QColor(50, 50, 50)
|
|
266
|
+
self.setStyleSheet("""QWidget { background-color: #1e1e1e; color: #d4d4d4; }""")
|
|
267
|
+
self._reapply_highlights_for_theme()
|
|
268
|
+
self.leftEdit.apply_theme_to_editor(dark=self.is_dark)
|
|
269
|
+
self.rightEdit.apply_theme_to_editor(dark=self.is_dark)
|
|
270
|
+
|
|
271
|
+
def set_light_theme(self):
|
|
272
|
+
"""
|
|
273
|
+
Apply light theme colors.
|
|
274
|
+
套用淺色主題配色。
|
|
275
|
+
"""
|
|
276
|
+
self.is_dark = False
|
|
277
|
+
self.color_del = QColor(255, 230, 230)
|
|
278
|
+
self.color_add = QColor(230, 255, 230)
|
|
279
|
+
self.color_hunk = QColor(230, 230, 255)
|
|
280
|
+
self.color_header = QColor(240, 240, 240)
|
|
281
|
+
self.setStyleSheet("""QWidget { background-color: white; color: black; }""")
|
|
282
|
+
self._reapply_highlights_for_theme()
|
|
283
|
+
self.leftEdit.apply_theme_to_editor(dark=self.is_dark)
|
|
284
|
+
self.rightEdit.apply_theme_to_editor(dark=self.is_dark)
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from PySide6.QtGui import QStandardItemModel, QStandardItem
|
|
2
|
+
from PySide6.QtWidgets import QTableView
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommitTable(QTableView):
|
|
6
|
+
"""
|
|
7
|
+
CommitTable 類別:用來顯示 Git Commit 紀錄的表格
|
|
8
|
+
CommitTable class: A table view to display Git commit history
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, parent=None):
|
|
12
|
+
super().__init__(parent)
|
|
13
|
+
|
|
14
|
+
# 建立一個 QStandardItemModel,初始為 0 行 5 欄
|
|
15
|
+
# Create a QStandardItemModel with 0 rows and 5 columns
|
|
16
|
+
self.model_data = QStandardItemModel(0, 5, self) # 多一欄 / 5 columns
|
|
17
|
+
|
|
18
|
+
# 設定表頭標籤
|
|
19
|
+
# Set horizontal header labels
|
|
20
|
+
self.model_data.setHorizontalHeaderLabels(["#", "SHA", "Message", "Author", "Date"])
|
|
21
|
+
|
|
22
|
+
# 將模型綁定到 QTableView
|
|
23
|
+
# Set the model for QTableView
|
|
24
|
+
self.setModel(self.model_data)
|
|
25
|
+
|
|
26
|
+
# 設定選擇行為為整列選取
|
|
27
|
+
# Set selection behavior to select entire rows
|
|
28
|
+
self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
29
|
+
|
|
30
|
+
# 禁止編輯表格內容
|
|
31
|
+
# Disable editing of table cells
|
|
32
|
+
self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
|
33
|
+
|
|
34
|
+
# 讓最後一欄自動延展填滿
|
|
35
|
+
# Stretch the last column to fill available space
|
|
36
|
+
self.horizontalHeader().setStretchLastSection(True)
|
|
37
|
+
|
|
38
|
+
def set_commits(self, commits):
|
|
39
|
+
"""
|
|
40
|
+
將 commit 資料填入表格
|
|
41
|
+
Populate the table with commit data
|
|
42
|
+
:param commits: commit 資料清單 (list of dicts with sha, message, author, date)
|
|
43
|
+
"""
|
|
44
|
+
# 清空現有資料
|
|
45
|
+
# Clear existing rows
|
|
46
|
+
self.model_data.setRowCount(0)
|
|
47
|
+
|
|
48
|
+
# 遍歷 commit 清單,逐行加入表格
|
|
49
|
+
# Iterate over commits and append rows
|
|
50
|
+
for index, commit in enumerate(commits, start=1):
|
|
51
|
+
row = [
|
|
52
|
+
QStandardItem(str(index)), # 行號 / row number
|
|
53
|
+
QStandardItem(commit["sha"][:7]), # SHA 短碼 / short SHA
|
|
54
|
+
QStandardItem(commit["message"]), # 提交訊息 / commit message
|
|
55
|
+
QStandardItem(commit["author"]), # 作者 / author
|
|
56
|
+
QStandardItem(commit["date"]), # 日期 / date
|
|
57
|
+
]
|
|
58
|
+
# 設定每個欄位不可編輯
|
|
59
|
+
# Make each item non-editable
|
|
60
|
+
for item in row:
|
|
61
|
+
item.setEditable(False)
|
|
62
|
+
|
|
63
|
+
# 將這一列加入模型
|
|
64
|
+
# Append row to the model
|
|
65
|
+
self.model_data.appendRow(row)
|