chartflow 0.1__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 (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. chartflow-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1007 @@
1
+ """
2
+ 代码编辑器组件 - 增强版
3
+ 基于QPlainTextEdit的高级Python代码编辑器
4
+
5
+ 增强特性:
6
+ - 优化的代码补全响应速度
7
+ - Consolas字体12号
8
+ """
9
+
10
+ from PyQt6.QtWidgets import (
11
+ QPlainTextEdit, QWidget, QVBoxLayout, QHBoxLayout,
12
+ QScrollBar, QAbstractSlider
13
+ )
14
+ from PyQt6.QtCore import (
15
+ Qt, QRect, QRectF, QSize, pyqtSignal, QTimer, QPoint
16
+ )
17
+ from PyQt6.QtGui import (
18
+ QPainter, QColor, QFont, QFontMetrics, QKeyEvent,
19
+ QMouseEvent, QWheelEvent, QTextCursor, QTextFormat,
20
+ QKeySequence, QAction, QTextDocument
21
+ )
22
+ from typing import Optional, List, Dict, Callable
23
+
24
+ from .syntax_highlighter import PythonSyntaxHighlighter
25
+ from .line_number_area import LineNumberArea, LineNumberDelegate
26
+ from .code_folder import CodeFolder, FoldRegion, FoldMarkerPainter
27
+ from .completer import CodeCompleter, SmartCompleter, CompletionItem
28
+ from .context_analyzer import ContextAnalyzer
29
+
30
+
31
+ class CodeEditor(QPlainTextEdit):
32
+ """
33
+ 高级代码编辑器
34
+
35
+ 功能:
36
+ - Python语法高亮
37
+ - 代码折叠
38
+ - 行号显示
39
+ - 代码补全(优化响应速度)
40
+ - 智能缩进
41
+ """
42
+
43
+ # 信号
44
+ text_modified = pyqtSignal() # 文本修改信号
45
+ cursor_position_changed = pyqtSignal(int, int) # 光标位置改变 (line, column)
46
+ fold_changed = pyqtSignal(int, bool) # 折叠状态改变
47
+ completions_requested = pyqtSignal(str, int) # 请求补全
48
+
49
+ # 编辑器配置
50
+ TAB_WIDTH = 4
51
+ FONT_FAMILY = 'Consolas'
52
+ FONT_SIZE = 14
53
+
54
+ # 颜色配置(浅色主题)
55
+ # 浅色代码面板
56
+ COLORS = {
57
+ 'background': '#ffffff',
58
+ 'text': '#333333',
59
+ 'selection': '#add6ff',
60
+ 'current_line': '#f5f5f5',
61
+ 'line_number_bg': '#f0f0f0',
62
+ 'line_number_text': '#999999',
63
+ 'fold_marker': '#999999',
64
+ 'whitespace': '#d0d0d0',
65
+ }
66
+
67
+ def __init__(self, parent=None):
68
+ super().__init__(parent)
69
+
70
+ # 代码折叠
71
+ self._folder = CodeFolder()
72
+ self._fold_marker_painter = FoldMarkerPainter(self._folder)
73
+
74
+ # 上下文分析
75
+ self._context_analyzer = ContextAnalyzer()
76
+
77
+ # 行号边栏(必须在_setup_font之前创建)
78
+ self._line_number_area = LineNumberArea(editor=self, parent=self)
79
+ print(f"[CodeEditor] LineNumberArea created, parent={self}")
80
+
81
+ # 初始化组件
82
+ self._setup_ui()
83
+ self._setup_font()
84
+ self._setup_highlighter()
85
+ self._setup_completer()
86
+ self._setup_connections()
87
+
88
+ # 连接信号
89
+ self._folder.fold_changed.connect(self._on_fold_changed)
90
+ self._line_number_area.fold_clicked.connect(self._on_fold_clicked)
91
+ self._line_number_area.line_clicked.connect(self._on_line_clicked)
92
+
93
+ # 更新行号区域
94
+ self._line_number_area.set_line_count(self.blockCount())
95
+ self._update_line_number_area_width()
96
+ self._update_line_number_area_geometry()
97
+
98
+ # 补全延迟计时器 - 优化为更短的延迟
99
+ self._completion_timer = QTimer()
100
+ self._completion_timer.setSingleShot(True)
101
+ self._completion_timer.timeout.connect(self._show_completions)
102
+
103
+ # 隐藏代码上下文
104
+ self._context_code_a: str = "" # 类定义代码
105
+ self._context_code_b: str = "" # 函数定义代码
106
+ self._context_prefix: str = "" # 前缀缩进
107
+
108
+ # 当前补全前缀
109
+ self._current_completion_prefix: str = ""
110
+
111
+ # 最后补全时间(用于性能优化)
112
+ self._last_completion_time = 0
113
+
114
+ # 补全防抖间隔(毫秒)
115
+ self._completion_debounce_ms = 50
116
+
117
+ def _setup_ui(self):
118
+ """设置UI"""
119
+ # 设置背景色
120
+ self.setStyleSheet(f"""
121
+ QPlainTextEdit {{
122
+ background-color: {self.COLORS['background']};
123
+ color: {self.COLORS['text']};
124
+ border: none;
125
+ selection-background-color: {self.COLORS['selection']};
126
+ font-family: {self.FONT_FAMILY};
127
+ font-size: {self.FONT_SIZE}px;
128
+ }}
129
+ """)
130
+
131
+ # 启用当前行高亮
132
+ self.setCenterOnScroll(True)
133
+
134
+ # 设置Tab宽度
135
+ self.setTabStopDistance(self.fontMetrics().horizontalAdvance(' ') * self.TAB_WIDTH)
136
+
137
+ # 启用自动换行
138
+ self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
139
+
140
+ # 设置光标宽度
141
+ self.setCursorWidth(2)
142
+
143
+ def _setup_font(self):
144
+ """设置字体 - Consolas 14号"""
145
+ font = QFont(self.FONT_FAMILY, self.FONT_SIZE)
146
+ font.setFixedPitch(True)
147
+ font.setStyleHint(QFont.StyleHint.Monospace)
148
+ self.setFont(font)
149
+
150
+ # 更新行号区域字体
151
+ self._line_number_area.set_font(font)
152
+ # 行高将在 _sync_line_height 中同步
153
+
154
+ def _setup_highlighter(self):
155
+ """设置语法高亮"""
156
+ self._highlighter = PythonSyntaxHighlighter(self.document())
157
+
158
+ def _setup_completer(self):
159
+ """设置代码补全"""
160
+ self._completer_manager = CodeCompleter()
161
+
162
+ self._completer = SmartCompleter(self)
163
+ self._completer.setWidget(self)
164
+ self._completer.activated_item.connect(self._on_completion_activated)
165
+
166
+ def _setup_connections(self):
167
+ """设置信号连接"""
168
+ # 文本修改
169
+ self.textChanged.connect(self._on_text_changed)
170
+
171
+ # 光标位置改变
172
+ self.cursorPositionChanged.connect(self._on_cursor_position_changed)
173
+
174
+ # 滚动
175
+ self.verticalScrollBar().valueChanged.connect(
176
+ self._on_vertical_scroll_changed
177
+ )
178
+
179
+ # 更新请求
180
+ self.updateRequest.connect(self._on_update_request)
181
+
182
+ # 块计数改变
183
+ self.blockCountChanged.connect(self._on_block_count_changed)
184
+
185
+ def set_context_code(self, code_a: str = "", code_b: str = "", prefix: str = ""):
186
+ """
187
+ 设置上下文代码
188
+
189
+ Args:
190
+ code_a: 类定义代码 (CA)
191
+ code_b: 函数定义代码 (CB)
192
+ prefix: 前缀缩进
193
+ """
194
+ print(f"[CodeEditor.set_context_code] Setting context code")
195
+ print(f"[CodeEditor.set_context_code] code_a length: {len(code_a)}")
196
+ print(f"[CodeEditor.set_context_code] code_b length: {len(code_b)}")
197
+ print(f"[CodeEditor.set_context_code] prefix: {repr(prefix)}")
198
+
199
+ self._context_code_a = code_a
200
+ self._context_code_b = code_b
201
+ self._context_prefix = prefix
202
+
203
+ # 更新补全器和分析器
204
+ self._completer_manager.set_context_code(code_a, code_b, prefix)
205
+ self._context_analyzer.set_context(code_a, code_b, prefix, self.toPlainText())
206
+
207
+ # 重新分析代码折叠
208
+ self._analyze_folding()
209
+
210
+ def _analyze_folding(self):
211
+ """分析代码折叠"""
212
+ # 只分析可见代码(避免上下文代码干扰行号)
213
+ visible_code = self.toPlainText()
214
+ self._folder.analyze_code(visible_code)
215
+
216
+ # 更新行号区域的可折叠行
217
+ foldable_lines = [r.start_line for r in self._folder.get_regions()]
218
+ self._line_number_area.set_foldable_lines(foldable_lines)
219
+
220
+ def _build_full_code(self) -> str:
221
+ """构建完整代码"""
222
+ lines = []
223
+
224
+ if self._context_code_a:
225
+ lines.append(self._context_code_a)
226
+ lines.append("")
227
+
228
+ if self._context_code_b:
229
+ lines.append(self._context_code_b)
230
+ lines.append("")
231
+
232
+ visible_code = self.toPlainText()
233
+ if self._context_prefix:
234
+ for line in visible_code.split('\n'):
235
+ lines.append(self._context_prefix + line)
236
+ else:
237
+ lines.append(visible_code)
238
+
239
+ return '\n'.join(lines)
240
+
241
+ def resizeEvent(self, event):
242
+ """调整大小事件"""
243
+ super().resizeEvent(event)
244
+
245
+ # 更新行号区域位置
246
+ self._update_line_number_area_geometry()
247
+
248
+ def _update_line_number_area_geometry(self):
249
+ """更新行号区域几何"""
250
+ cr = self.contentsRect()
251
+ width = self._line_number_area.width()
252
+ # 行号区域放在最左侧,与编辑器内容区对齐
253
+ self._line_number_area.setGeometry(
254
+ cr.left(), cr.top(),
255
+ width, cr.height()
256
+ )
257
+ print(f"[CodeEditor] LineNumberArea geometry: x={cr.left()}, y={cr.top()}, w={width}, h={cr.height()}")
258
+
259
+ def _update_line_number_area_width(self):
260
+ """更新行号区域宽度"""
261
+ width = self._line_number_area.width()
262
+ self.setViewportMargins(width, 0, 0, 0)
263
+ print(f"[CodeEditor] viewport margins set: {width}")
264
+
265
+ def _on_block_count_changed(self, count: int):
266
+ """块计数改变"""
267
+ self._line_number_area.set_line_count(count)
268
+ self._update_line_number_area_width()
269
+ self._analyze_folding()
270
+ # 同步行高
271
+ QTimer.singleShot(0, self._sync_line_height)
272
+
273
+ def _sync_line_height(self):
274
+ """同步行号区域行高与 QPlainTextEdit 实际行高"""
275
+ block = self.firstVisibleBlock()
276
+ if block.isValid():
277
+ block_rect = self.blockBoundingRect(block)
278
+ if block_rect.height() > 0:
279
+ actual_height = int(block_rect.height())
280
+ if actual_height != self._line_number_area._line_height:
281
+ self._line_number_area.set_line_height(actual_height)
282
+
283
+ def _on_update_request(self, rect: QRect, dy: int):
284
+ """更新请求"""
285
+ # 同步行高
286
+ self._sync_line_height()
287
+ # 行号区域现在直接从编辑器获取可见行,只需要重绘
288
+ self._line_number_area.update()
289
+
290
+ if rect.contains(self.viewport().rect()):
291
+ self._update_line_number_area_width()
292
+
293
+ def paintEvent(self, event):
294
+ """绘制事件"""
295
+ # 获取被折叠的行
296
+ collapsed_regions = [(r.start_line, r.end_line) for r in self._folder.get_regions() if r.collapsed]
297
+
298
+ if not collapsed_regions:
299
+ # 没有折叠,正常绘制
300
+ self._draw_current_line_highlight()
301
+ super().paintEvent(event)
302
+ return
303
+
304
+ # 有折叠,需要自定义绘制
305
+ self._paint_with_folding(event, collapsed_regions)
306
+
307
+ def _paint_with_folding(self, event, collapsed_regions):
308
+ """带折叠的自定义绘制 - 压缩被折叠的行"""
309
+ painter = QPainter(self.viewport())
310
+ painter.setClipRect(event.rect())
311
+
312
+ # 构建被折叠的行集合
313
+ folded_lines = set()
314
+ fold_ranges = []
315
+ for start, end in collapsed_regions:
316
+ folded_lines.update(range(start + 1, end + 1))
317
+ fold_ranges.append((start, end))
318
+
319
+ # 计算折叠导致的y偏移
320
+ # 对于每一行,计算在它之前有多少行被折叠了
321
+ # 被折叠的内部行是 (start+1) 到 end
322
+ line_height = self._line_number_area._line_height
323
+ def get_y_offset(line):
324
+ offset = 0
325
+ for start, end in fold_ranges:
326
+ if line > start:
327
+ if line > end:
328
+ # 完全在折叠区域之后,减去所有被折叠的内部行
329
+ offset += (end - start) * line_height
330
+ # 如果在折叠区域内(start < line <= end),正常显示,不偏移
331
+ return offset
332
+
333
+ # 绘制可见的块
334
+ block = self.firstVisibleBlock()
335
+ while block.isValid():
336
+ block_num = block.blockNumber() + 1
337
+ block_rect = self.blockBoundingGeometry(block)
338
+
339
+ # 如果被折叠,跳过不绘制
340
+ if block_num in folded_lines:
341
+ block = block.next()
342
+ continue
343
+
344
+ # 计算调整后的y位置
345
+ y_offset = get_y_offset(block_num)
346
+ adjusted_rect = block_rect.translated(0, -y_offset)
347
+ adjusted_rect.translate(self.contentOffset())
348
+
349
+ # 如果块在视口外,停止
350
+ if adjusted_rect.top() > self.viewport().height():
351
+ break
352
+
353
+ # 绘制当前行高亮
354
+ if block_num == self.textCursor().blockNumber() + 1:
355
+ painter.fillRect(adjusted_rect, QColor(self.COLORS['current_line']))
356
+
357
+ # 绘制块内容
358
+ self._draw_block_content(painter, block, adjusted_rect)
359
+
360
+ # 如果是折叠起始行,绘制折叠占位符
361
+ for start, end in fold_ranges:
362
+ if block_num == start:
363
+ self._draw_single_fold_placeholder(
364
+ painter, block, adjusted_rect, start, end
365
+ )
366
+
367
+ block = block.next()
368
+
369
+ painter.end()
370
+
371
+ def _draw_block_content(self, painter: QPainter, block, rect: QRect):
372
+ """绘制块内容"""
373
+ layout = block.layout()
374
+ if not layout:
375
+ return
376
+
377
+ for i in range(layout.lineCount()):
378
+ line = layout.lineAt(i)
379
+ line_rect = QRectF(
380
+ rect.x() + line.x(),
381
+ rect.y() + line.y(),
382
+ line.width(),
383
+ line.height()
384
+ )
385
+ line.draw(painter, line_rect.topLeft())
386
+
387
+ def _draw_single_fold_placeholder(self, painter: QPainter, block, rect: QRect,
388
+ start_line: int, end_line: int):
389
+ """绘制单个折叠占位符"""
390
+ x = int(rect.x())
391
+ y = int(rect.y())
392
+ width = int(self.viewport().width() - x - 20)
393
+ height = int(rect.height())
394
+
395
+ # 绘制灰色圆角矩形占位符
396
+ painter.setPen(Qt.PenStyle.NoPen)
397
+ painter.setBrush(QColor('#e8e8e8'))
398
+ painter.drawRoundedRect(x, y, width, height, 4, 4)
399
+
400
+ # 绘制边框
401
+ painter.setPen(QColor('#c0c0c0'))
402
+ painter.setBrush(Qt.BrushStyle.NoBrush)
403
+ painter.drawRoundedRect(x, y, width, height, 4, 4)
404
+
405
+ # 绘制折叠的文本内容
406
+ text = block.text().strip()
407
+ if len(text) > 50:
408
+ text = text[:50] + '...'
409
+
410
+ painter.setPen(QColor('#666666'))
411
+ font = QFont(self.FONT_FAMILY, self.FONT_SIZE)
412
+ painter.setFont(font)
413
+
414
+ text_x = x + 10
415
+ text_y = y + height // 2 + 5
416
+ painter.drawText(text_x, text_y, text)
417
+
418
+ # 绘制三个小圆点
419
+ dot_y = y + height // 2
420
+ dot_spacing = 6
421
+ dot_radius = 2
422
+ painter.setPen(Qt.PenStyle.NoPen)
423
+ painter.setBrush(QColor('#999999'))
424
+
425
+ text_width = QFontMetrics(font).horizontalAdvance(text)
426
+ dots_x = text_x + text_width + 15
427
+
428
+ for i in range(3):
429
+ dot_x = dots_x + i * dot_spacing
430
+ painter.drawEllipse(dot_x - dot_radius, dot_y - dot_radius,
431
+ dot_radius * 2, dot_radius * 2)
432
+
433
+ def _draw_fold_placeholders(self):
434
+ """绘制折叠占位符(灰色圆角矩形 + 三个小圆点)"""
435
+ collapsed_regions = [r for r in self._folder.get_regions() if r.collapsed]
436
+ if not collapsed_regions:
437
+ return
438
+
439
+ painter = QPainter(self.viewport())
440
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
441
+
442
+ for region in collapsed_regions:
443
+ # 获取折叠起始行和结束行
444
+ start_block = self.document().findBlockByNumber(region.start_line - 1)
445
+ end_block = self.document().findBlockByNumber(region.end_line - 1)
446
+
447
+ if not start_block.isValid():
448
+ continue
449
+
450
+ # 计算位置
451
+ start_rect = self.blockBoundingGeometry(start_block)
452
+ start_rect.translate(self.contentOffset())
453
+
454
+ if end_block.isValid():
455
+ end_rect = self.blockBoundingGeometry(end_block)
456
+ end_rect.translate(self.contentOffset())
457
+ # 折叠区域高度(从起始行底部到结束行底部)
458
+ fold_height = end_rect.bottom() - start_rect.bottom()
459
+ else:
460
+ fold_height = 0
461
+
462
+ x = int(start_rect.x())
463
+ y_start = int(start_rect.y())
464
+ width = int(self.viewport().width() - x - 20)
465
+ line_height = int(start_rect.height())
466
+
467
+ # 1. 先用白色背景覆盖被折叠的代码行(隐藏原代码)
468
+ if fold_height > 0:
469
+ painter.setPen(Qt.PenStyle.NoPen)
470
+ painter.setBrush(QColor('#ffffff'))
471
+ painter.drawRect(x, int(start_rect.bottom()), width, int(fold_height))
472
+
473
+ # 2. 绘制灰色圆角矩形占位符(在起始行位置,覆盖原第一行)
474
+ painter.setBrush(QColor('#e8e8e8'))
475
+ painter.drawRoundedRect(x, y_start, width, line_height, 4, 4)
476
+
477
+ # 3. 绘制边框
478
+ painter.setPen(QColor('#c0c0c0'))
479
+ painter.setBrush(Qt.BrushStyle.NoBrush)
480
+ painter.drawRoundedRect(x, y_start, width, line_height, 4, 4)
481
+
482
+ # 4. 绘制折叠的文本内容(第一行)
483
+ text = start_block.text().strip()
484
+ if len(text) > 50:
485
+ text = text[:50] + '...'
486
+
487
+ painter.setPen(QColor('#666666'))
488
+ font = QFont(self.FONT_FAMILY, self.FONT_SIZE)
489
+ painter.setFont(font)
490
+
491
+ text_x = x + 10
492
+ text_y = y_start + line_height // 2 + 5
493
+ painter.drawText(text_x, text_y, text)
494
+
495
+ # 5. 绘制三个小圆点
496
+ dot_y = y_start + line_height // 2
497
+ dot_spacing = 6
498
+ dot_radius = 2
499
+ painter.setPen(Qt.PenStyle.NoPen)
500
+ painter.setBrush(QColor('#999999'))
501
+
502
+ text_width = QFontMetrics(font).horizontalAdvance(text)
503
+ dots_x = text_x + text_width + 15
504
+
505
+ for i in range(3):
506
+ dot_x = dots_x + i * dot_spacing
507
+ painter.drawEllipse(dot_x - dot_radius, dot_y - dot_radius,
508
+ dot_radius * 2, dot_radius * 2)
509
+
510
+ painter.end()
511
+
512
+ def _draw_current_line_highlight(self):
513
+ """绘制当前行高亮"""
514
+ # 检查当前行是否被折叠
515
+ cursor = self.textCursor()
516
+ block = cursor.block()
517
+ line = block.blockNumber() + 1
518
+
519
+ # 如果被折叠,不绘制高亮
520
+ for region in self._folder.get_regions():
521
+ if region.collapsed and region.contains(line):
522
+ return
523
+
524
+ if not block.isValid():
525
+ return
526
+
527
+ # 获取当前行的几何信息
528
+ layout = block.layout()
529
+ if not layout:
530
+ return
531
+
532
+ # 获取当前块在文档中的位置
533
+ block_rect = self.blockBoundingGeometry(block)
534
+ block_rect.translate(self.contentOffset())
535
+
536
+ # 绘制高亮背景
537
+ painter = QPainter(self.viewport())
538
+ painter.fillRect(block_rect, QColor(self.COLORS['current_line']))
539
+ painter.end()
540
+
541
+ def _draw_fold_markers(self):
542
+ """绘制折叠标记"""
543
+ painter = QPainter(self.viewport())
544
+
545
+ # 获取可见区域
546
+ first_visible = self.firstVisibleBlock().blockNumber() + 1
547
+ last_visible = first_visible
548
+
549
+ block = self.firstVisibleBlock()
550
+ while block.isValid():
551
+ rect = self.blockBoundingGeometry(block)
552
+ rect.translate(self.contentOffset())
553
+
554
+ if rect.top() > self.viewport().height():
555
+ break
556
+
557
+ last_visible = block.blockNumber() + 1
558
+ block = block.next()
559
+
560
+ # 绘制每个折叠区域的标记
561
+ for region in self._folder.get_regions():
562
+ if first_visible <= region.start_line <= last_visible:
563
+ # 计算位置
564
+ block = self.document().findBlockByNumber(region.start_line - 1)
565
+ rect = self.blockBoundingGeometry(block)
566
+ rect.translate(self.contentOffset())
567
+
568
+ # 绘制标记
569
+ x = 4
570
+ y = rect.top()
571
+ height = rect.height()
572
+
573
+ self._fold_marker_painter.paint_marker(
574
+ painter, region, x, y, height
575
+ )
576
+
577
+ painter.end()
578
+
579
+ def _on_text_changed(self):
580
+ """文本改变"""
581
+ self.text_modified.emit()
582
+
583
+ # 延迟分析折叠
584
+ QTimer.singleShot(500, self._analyze_folding)
585
+
586
+ # 触发补全
587
+ self._trigger_completion()
588
+
589
+ def _on_cursor_position_changed(self):
590
+ """光标位置改变"""
591
+ cursor = self.textCursor()
592
+ line = cursor.blockNumber() + 1
593
+ column = cursor.columnNumber()
594
+
595
+ self.cursor_position_changed.emit(line, column)
596
+
597
+ # 更新当前行
598
+ self._line_number_area.set_current_line(line)
599
+
600
+ # 隐藏补全窗口(如果光标移动较远)
601
+ if self._completer.popup().isVisible():
602
+ # 检查是否还在当前词附近
603
+ current_word = self._get_current_word()
604
+ if not current_word.startswith(self._current_completion_prefix):
605
+ self._completer.popup().hide()
606
+
607
+ def _on_vertical_scroll_changed(self, value: int):
608
+ """垂直滚动改变"""
609
+ self._line_number_area.update()
610
+
611
+ def _on_fold_clicked(self, line: int):
612
+ """折叠按钮点击"""
613
+ self._folder.toggle_fold(line)
614
+ self._line_number_area.toggle_fold(line)
615
+ self.fold_changed.emit(line, self._folder.get_region_at_line(line).collapsed
616
+ if self._folder.get_region_at_line(line) else False)
617
+ self.update()
618
+
619
+ def _on_line_clicked(self, line: int):
620
+ """行号点击"""
621
+ # 移动光标到指定行
622
+ block = self.document().findBlockByNumber(line - 1)
623
+ if block.isValid():
624
+ cursor = QTextCursor(block)
625
+ self.setTextCursor(cursor)
626
+ self.setFocus()
627
+
628
+ def _on_fold_changed(self, line: int, collapsed: bool):
629
+ """折叠状态改变"""
630
+ self._line_number_area.set_folded_lines([
631
+ r.start_line for r in self._folder.get_regions() if r.collapsed
632
+ ])
633
+ self.update()
634
+
635
+ def keyPressEvent(self, event: QKeyEvent):
636
+ """键盘按下事件"""
637
+ key = event.key()
638
+ modifiers = event.modifiers()
639
+
640
+ # 处理折叠快捷键
641
+ if modifiers == Qt.KeyboardModifier.ControlModifier:
642
+ if key == Qt.Key.Key_Minus or key == Qt.Key.Key_Underscore:
643
+ # Ctrl + - : 折叠当前块
644
+ self._fold_current_block()
645
+ return
646
+ elif key == Qt.Key.Key_Plus or key == Qt.Key.Key_Equal:
647
+ # Ctrl + + : 展开当前块
648
+ self._unfold_current_block()
649
+ return
650
+
651
+ if modifiers == (Qt.KeyboardModifier.ControlModifier |
652
+ Qt.KeyboardModifier.ShiftModifier):
653
+ if key == Qt.Key.Key_Minus or key == Qt.Key.Key_Underscore:
654
+ # Ctrl + Shift + - : 折叠所有
655
+ self._folder.fold_all()
656
+ self.update()
657
+ return
658
+ elif key == Qt.Key.Key_Plus or key == Qt.Key.Key_Equal:
659
+ # Ctrl + Shift + + : 展开所有
660
+ self._folder.unfold_all()
661
+ self.update()
662
+ return
663
+
664
+ # 处理补全导航 - 将事件传递给补全器处理(必须在 Tab/Enter 处理之前)
665
+ if self._completer.popup() and self._completer.popup().isVisible():
666
+ if key == Qt.Key.Key_Escape:
667
+ # ESC 关闭补全窗口
668
+ self._completer.popup().hide()
669
+ event.accept()
670
+ return
671
+ elif key in (Qt.Key.Key_Up, Qt.Key.Key_Down,
672
+ Qt.Key.Key_Return, Qt.Key.Key_Enter,
673
+ Qt.Key.Key_Tab):
674
+ # 这些键交给补全器处理
675
+ # QCompleter 会自动处理 Up/Down/Enter
676
+ # 对于 Tab,我们需要手动触发补全
677
+ if key == Qt.Key.Key_Tab:
678
+ current_item = self._completer.get_current_item()
679
+ if current_item:
680
+ self._on_completion_activated(current_item)
681
+ self._completer.popup().hide()
682
+ event.accept()
683
+ return
684
+ # 让 QCompleter 处理其他键
685
+ event.ignore()
686
+ return
687
+ elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
688
+ # 左右箭头关闭补全窗口并正常处理
689
+ self._completer.popup().hide()
690
+ # 继续正常处理箭头键
691
+
692
+ # 处理Tab键(智能缩进)- 只在补全窗口不可见时
693
+ if key == Qt.Key.Key_Tab and not event.modifiers():
694
+ self._handle_tab()
695
+ return
696
+
697
+ # 处理Enter键(自动缩进)- 只在补全窗口不可见时
698
+ if key == Qt.Key.Key_Return or key == Qt.Key.Key_Enter:
699
+ self._handle_enter()
700
+ return
701
+
702
+ # 处理补全快捷键
703
+ if key == Qt.Key.Key_Space and modifiers == Qt.KeyboardModifier.ControlModifier:
704
+ self._show_completions()
705
+ return
706
+
707
+ # 调用父类的处理
708
+ super().keyPressEvent(event)
709
+
710
+ # 触发补全(优化:只在特定键后触发)
711
+ if self._should_trigger_completion(key):
712
+ self._trigger_completion()
713
+
714
+ def _should_trigger_completion(self, key: int) -> bool:
715
+ """判断是否应该触发补全"""
716
+ # 字母数字下划线
717
+ if Qt.Key.Key_A <= key <= Qt.Key.Key_Z or \
718
+ Qt.Key.Key_0 <= key <= Qt.Key.Key_9 or \
719
+ key == Qt.Key.Key_Underscore:
720
+ return True
721
+
722
+ # 点号(属性访问)
723
+ if key == Qt.Key.Key_Period:
724
+ return True
725
+
726
+ return False
727
+
728
+ def _handle_tab(self):
729
+ """处理Tab键"""
730
+ cursor = self.textCursor()
731
+
732
+ if cursor.hasSelection():
733
+ # 有多行选择,进行缩进/取消缩进
734
+ self._indent_selection()
735
+ else:
736
+ # 单行,插入空格
737
+ cursor.insertText(' ' * self.TAB_WIDTH)
738
+
739
+ def _indent_selection(self):
740
+ """缩进选中的行"""
741
+ cursor = self.textCursor()
742
+ start = cursor.selectionStart()
743
+ end = cursor.selectionEnd()
744
+
745
+ # 获取选中的块
746
+ start_block = self.document().findBlock(start)
747
+ end_block = self.document().findBlock(end)
748
+
749
+ cursor.beginEditBlock()
750
+
751
+ block = start_block
752
+ while block.isValid() and block.blockNumber() <= end_block.blockNumber():
753
+ block_cursor = QTextCursor(block)
754
+ block_cursor.movePosition(QTextCursor.MoveOperation.StartOfBlock)
755
+ block_cursor.insertText(' ' * self.TAB_WIDTH)
756
+ block = block.next()
757
+
758
+ cursor.endEditBlock()
759
+
760
+ def _handle_enter(self):
761
+ """处理Enter键"""
762
+ cursor = self.textCursor()
763
+
764
+ # 获取当前行的缩进
765
+ block = cursor.block()
766
+ line_text = block.text()
767
+ indent = len(line_text) - len(line_text.lstrip())
768
+
769
+ # 检查是否需要增加缩进
770
+ stripped = line_text.strip()
771
+ if stripped.endswith(':'):
772
+ indent += self.TAB_WIDTH
773
+
774
+ # 插入换行和缩进
775
+ cursor.insertText('\n' + ' ' * indent)
776
+
777
+ def _fold_current_block(self):
778
+ """折叠当前块"""
779
+ cursor = self.textCursor()
780
+ line = cursor.blockNumber() + 1
781
+
782
+ # 查找包含当前行的折叠区域
783
+ for region in self._folder.get_regions():
784
+ if region.contains(line):
785
+ if not region.collapsed:
786
+ self._folder.fold_region(region.start_line)
787
+ self._line_number_area.set_folded_lines([
788
+ r.start_line for r in self._folder.get_regions() if r.collapsed
789
+ ])
790
+ self.update()
791
+ return
792
+
793
+ # 如果当前行就是折叠开始行
794
+ self._folder.fold_region(line)
795
+ self._line_number_area.toggle_fold(line)
796
+ self.update()
797
+
798
+ def _unfold_current_block(self):
799
+ """展开当前块"""
800
+ cursor = self.textCursor()
801
+ line = cursor.blockNumber() + 1
802
+
803
+ self._folder.unfold_region(line)
804
+ self._line_number_area.toggle_fold(line)
805
+ self.update()
806
+
807
+ def _trigger_completion(self):
808
+ """触发代码补全 - 优化版本"""
809
+ # 取消之前的计时器
810
+ self._completion_timer.stop()
811
+
812
+ # 检查是否需要显示补全
813
+ cursor = self.textCursor()
814
+ pos = cursor.position()
815
+ text = self.toPlainText()
816
+
817
+ # 获取当前词
818
+ current_word = self._get_current_word()
819
+
820
+ # 检查是否在点号后
821
+ after_dot = self._is_after_dot()
822
+
823
+ # 如果当前词太短且不在点号后,不显示补全
824
+ if len(current_word) < 1 and not after_dot:
825
+ self._completer.popup().hide()
826
+ return
827
+
828
+ # 保存当前补全前缀
829
+ self._current_completion_prefix = current_word
830
+
831
+ # 如果在点号后,立即显示补全
832
+ if after_dot:
833
+ self._show_completions()
834
+ else:
835
+ # 否则使用短延迟
836
+ self._completion_timer.start(self._completion_debounce_ms)
837
+
838
+ def _show_completions(self):
839
+ """显示补全列表 - 优化版本"""
840
+ cursor = self.textCursor()
841
+ pos = cursor.position()
842
+ text = self.toPlainText()
843
+
844
+ # 获取补全建议
845
+ completions = self._completer_manager.get_completions(text, pos)
846
+
847
+ if not completions:
848
+ popup = self._completer.popup()
849
+ if popup:
850
+ popup.hide()
851
+ return
852
+
853
+ # 如果只有一个补全项且与当前输入完全匹配,隐藏补全窗口
854
+ if len(completions) == 1 and self._current_completion_prefix:
855
+ if completions[0].text.lower() == self._current_completion_prefix.lower():
856
+ popup = self._completer.popup()
857
+ if popup:
858
+ popup.hide()
859
+ return
860
+
861
+ # 计算弹出位置 - 基于当前词的开始位置
862
+ cursor = self.textCursor()
863
+ cursor_rect = self.cursorRect()
864
+
865
+ # 如果有前缀词,调整位置到词的开始处(让补全窗口出现在输入位置)
866
+ if self._current_completion_prefix:
867
+ # 创建一个临时光标,移动到词的开始位置
868
+ temp_cursor = QTextCursor(cursor)
869
+ pos = cursor.position()
870
+ # 向前查找词的开始
871
+ start = pos - len(self._current_completion_prefix)
872
+ if start >= 0:
873
+ temp_cursor.setPosition(start)
874
+ cursor_rect = self.cursorRect(temp_cursor)
875
+
876
+ # 显示补全窗口 - cursorRect() 返回的是视口坐标
877
+ # 需要转换为全局坐标,然后 completer 会处理坐标映射
878
+ global_pos = self.viewport().mapToGlobal(cursor_rect.bottomLeft())
879
+
880
+ # QCompleter.complete() 需要相对于 setWidget() 的坐标
881
+ # 转换全局坐标到 widget 坐标
882
+ widget_pos = self.mapFromGlobal(global_pos)
883
+ popup_width = 300
884
+
885
+ popup_rect = QRect(widget_pos, QSize(popup_width, 200))
886
+
887
+ # 更新补全器并传入位置
888
+ self._completer.update_completions(completions, popup_rect)
889
+
890
+ def _on_completion_activated(self, item: CompletionItem):
891
+ """补全项被激活"""
892
+ cursor = self.textCursor()
893
+
894
+ # 删除当前词
895
+ current_word = self._get_current_word()
896
+ if current_word:
897
+ for _ in range(len(current_word)):
898
+ cursor.deletePreviousChar()
899
+
900
+ # 插入补全文本
901
+ cursor.insertText(item.text)
902
+
903
+ # 如果是函数/方法,添加括号
904
+ if item.item_type in ('function', 'method', 'builtin'):
905
+ cursor.insertText('()')
906
+ cursor.movePosition(QTextCursor.MoveOperation.PreviousCharacter)
907
+
908
+ self.setTextCursor(cursor)
909
+ popup = self._completer.popup()
910
+ if popup:
911
+ popup.hide()
912
+
913
+ def _get_current_word(self) -> str:
914
+ """获取光标处的当前词"""
915
+ cursor = self.textCursor()
916
+ pos = cursor.position()
917
+ text = self.toPlainText()
918
+
919
+ # 向前查找词的开始
920
+ start = pos - 1
921
+ while start >= 0 and (text[start].isalnum() or text[start] == '_'):
922
+ start -= 1
923
+ start += 1
924
+
925
+ return text[start:pos]
926
+
927
+ def _is_after_dot(self) -> bool:
928
+ """检查光标是否在点号后"""
929
+ cursor = self.textCursor()
930
+ pos = cursor.position()
931
+ text = self.toPlainText()
932
+
933
+ # 跳过空白
934
+ check_pos = pos - 1
935
+ while check_pos >= 0 and text[check_pos].isspace():
936
+ check_pos -= 1
937
+
938
+ return check_pos >= 0 and text[check_pos] == '.'
939
+
940
+ def mousePressEvent(self, event: QMouseEvent):
941
+ """鼠标按下事件"""
942
+ # 折叠功能现在只在左侧行号区域处理
943
+ super().mousePressEvent(event)
944
+
945
+ def get_text(self) -> str:
946
+ """获取文本"""
947
+ return self.toPlainText()
948
+
949
+ def set_text(self, text: str):
950
+ """设置文本"""
951
+ self.setPlainText(text)
952
+
953
+ def get_cursor_position(self) -> tuple:
954
+ """获取光标位置 (line, column)"""
955
+ cursor = self.textCursor()
956
+ return (cursor.blockNumber() + 1, cursor.columnNumber())
957
+
958
+ def set_cursor_position(self, line: int, column: int = 0):
959
+ """设置光标位置"""
960
+ block = self.document().findBlockByNumber(line - 1)
961
+ if block.isValid():
962
+ cursor = QTextCursor(block)
963
+ cursor.movePosition(QTextCursor.MoveOperation.Right,
964
+ QTextCursor.MoveMode.MoveAnchor, column)
965
+ self.setTextCursor(cursor)
966
+
967
+ def get_visible_lines(self) -> tuple:
968
+ """获取可见行范围 (first, last)"""
969
+ first = self.firstVisibleBlock().blockNumber()
970
+
971
+ block = self.firstVisibleBlock()
972
+ last = first
973
+ while block.isValid():
974
+ rect = self.blockBoundingGeometry(block)
975
+ rect.translate(self.contentOffset())
976
+
977
+ if rect.top() > self.viewport().height():
978
+ break
979
+
980
+ last = block.blockNumber()
981
+ block = block.next()
982
+
983
+ return (first + 1, last + 1)
984
+
985
+ def scroll_to_line(self, line: int):
986
+ """滚动到指定行"""
987
+ block = self.document().findBlockByNumber(line - 1)
988
+ if block.isValid():
989
+ cursor = QTextCursor(block)
990
+ self.setTextCursor(cursor)
991
+ self.ensureCursorVisible()
992
+
993
+ def fold_all(self):
994
+ """折叠所有"""
995
+ self._folder.fold_all()
996
+ self._line_number_area.set_folded_lines([
997
+ r.start_line for r in self._folder.get_regions() if r.collapsed
998
+ ])
999
+ self.update()
1000
+
1001
+ def unfold_all(self):
1002
+ """展开所有"""
1003
+ self._folder.unfold_all()
1004
+ self._line_number_area.set_folded_lines([
1005
+ r.start_line for r in self._folder.get_regions() if r.collapsed
1006
+ ])
1007
+ self.update()