Files
cowork-local/presentation/folder/code_editor.py
T
anhtnm1andClaude Opus 5 e29a0ccdbd refactor: vá 4 hồi quy, tách 4 file chạm trần LOC, docstring lên 100%
Hồi quy đã vá
-------------
F-12  Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
      `chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
      `composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
      tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
      là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
      gắn 1 tệp, khớp bản trước refactor.

F-01  Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
      `folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
      `ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
      tiên hỏng thì đổi provider chính là lúc phải thử lại.

F-07  Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
      lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
      người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
      theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
      mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
      `check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.

F-11  Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
      mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
      và không bao giờ chạy.

Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:

    graph_renderer.py         -> graph_scene_builder.py + graph_export.py
    co4e_workflow_service.py  -> co4e_run_history.py
    json_config_repository.py -> config_sections.py
    agents_admin_tab.py       -> shared/agent_kind_visuals.py

File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.

Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.

Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.

859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:41:45 +09:00

221 lines
9.5 KiB
Python

"""CodeEditor — the VS-Code-style code/text editor widget (R08-T12, split
out of ``document_preview_manager.py`` to keep that file under the 400-line
cap; originally ``ui/folder_tab.py``, lines 61-236 of the original
1587-line file: the Pygments token-colour helper, ``PygmentsHighlighter``,
``_LineNumbers``, ``CodeEditor``).
"""
from __future__ import annotations
from PySide6.QtCore import QRect, QSize, Qt, QTimer
from PySide6.QtGui import QColor, QFont, QPainter, QSyntaxHighlighter, QTextCharFormat
from PySide6.QtWidgets import QPlainTextEdit, QWidget
from cowork_local.theme import current_palette
_MAX_HIGHLIGHT_CHARS = 300_000 # skip colouring huge files (keeps typing snappy)
# ── VS-Code-Dark+-ish token palette ────────────────────────────────────────
def _fmt(color: str, *, italic: bool = False, bold: bool = False) -> QTextCharFormat:
"""Dựng một định dạng ký tự cho bộ tô màu cú pháp."""
f = QTextCharFormat()
f.setForeground(QColor(color))
if italic:
f.setFontItalic(True)
if bold:
f.setFontWeight(QFont.Bold)
return f
class PygmentsHighlighter(QSyntaxHighlighter):
"""Colour the whole document with Pygments and apply per-block. Re-lexes the
full text (debounced) so multi-line strings/comments colour correctly."""
def __init__(self, document):
"""Tô màu cú pháp bằng Pygments.
Tô lại sau một nhịp trễ (``QTimer`` chạy một lần) thay vì ngay mỗi phím: gõ
nhanh trong file lớn mà tô đồng bộ thì giao diện khựng.
"""
super().__init__(document)
from pygments.lexers.special import TextLexer
self._lexer = TextLexer(stripnl=False)
self._ranges: list[tuple[int, int, QTextCharFormat]] = []
self._rules = self._build_rules()
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(250)
self._timer.timeout.connect(self._retokenize)
document.contentsChanged.connect(self._timer.start)
@staticmethod
def _build_rules():
"""Dựng bảng ánh xạ loại token của Pygments sang màu theo theme đang dùng."""
from pygments.token import (
Comment, Error, Keyword, Name, Number, Operator, Punctuation, String,
)
p = current_palette()
return [
(Comment, _fmt(p.code_comment, italic=True)),
(Keyword.Type, _fmt(p.code_type)),
(Keyword, _fmt(p.code_keyword)),
(Name.Function, _fmt(p.code_func)),
(Name.Class, _fmt(p.code_type)),
(Name.Decorator, _fmt(p.code_func)),
(Name.Builtin, _fmt(p.code_type)),
(Name.Tag, _fmt(p.code_keyword)),
(Name.Attribute, _fmt(p.code_attr)),
(String.Doc, _fmt(p.code_comment, italic=True)),
(String, _fmt(p.code_string)),
(Number, _fmt(p.code_number)),
(Operator, _fmt(p.code_fg)),
(Punctuation, _fmt(p.code_fg)),
(Error, _fmt(p.code_error)),
]
def set_filename(self, filename: str, text: str = "") -> None:
"""Chọn bộ phân tích cú pháp theo đuôi tệp; không nhận ra thì tắt tô màu."""
from pygments.lexers import get_lexer_for_filename, guess_lexer
from pygments.lexers.special import TextLexer
from pygments.util import ClassNotFound
try:
self._lexer = get_lexer_for_filename(filename, stripnl=False)
except ClassNotFound:
try:
self._lexer = guess_lexer(text) if text.strip() else TextLexer()
except ClassNotFound:
self._lexer = TextLexer(stripnl=False)
self._retokenize()
def _fmt_for(self, tok):
"""Định dạng của một token; leo dần lên token cha cho tới khi khớp một luật."""
for ttype, fmt in self._rules:
if tok in ttype:
return fmt
return None
def _retokenize(self) -> None:
"""Phân tích lại toàn bộ tài liệu và ghi nhớ định dạng cho từng dòng.
Pygments phân tích theo cả tệp chứ không theo từng dòng, nên không thể
tô đúng nếu chỉ nhìn một dòng — ví dụ chuỗi nhiều dòng hay khối chú thích.
"""
from pygments import lex
text = self.document().toPlainText()
self._ranges = []
if len(text) <= _MAX_HIGHLIGHT_CHARS:
pos = 0
for tok, val in lex(text, self._lexer):
fmt = self._fmt_for(tok)
if fmt is not None and val:
self._ranges.append((pos, pos + len(val), fmt))
pos += len(val)
self.rehighlight()
def highlightBlock(self, text: str) -> None: # noqa: N802 - Qt override
"""Áp định dạng đã tính sẵn cho một dòng. Qt gọi hàm này cho từng dòng hiện trên màn."""
if not self._ranges:
return
bstart = self.currentBlock().position()
bend = bstart + len(text)
for start, end, fmt in self._ranges:
if end <= bstart or start >= bend:
continue
s = max(start, bstart) - bstart
e = min(end, bend) - bstart
if e > s:
self.setFormat(s, e - s, fmt)
class _LineNumbers(QWidget):
"""Máng số dòng vẽ bên trái ô soạn thảo."""
def __init__(self, editor):
"""Dải số dòng bên trái ô soạn mã."""
super().__init__(editor)
self._editor = editor
def sizeHint(self) -> QSize:
"""Bề rộng máng số dòng, do ô soạn thảo tính theo số chữ số của dòng cuối."""
return QSize(self._editor.line_number_width(), 0)
def paintEvent(self, event): # noqa: N802
"""Nhờ ô soạn thảo vẽ — nó mới biết dòng nào đang hiện ở đâu."""
self._editor.paint_line_numbers(event)
class CodeEditor(QPlainTextEdit):
"""A dark, monospaced editor with a line-number gutter + Pygments colouring —
the Sublime/VS-Code look for viewing & editing source files."""
def __init__(self):
"""Ô soạn mã: phông đều, không tự xuống dòng, tab rộng 4 ký tự.
Không tự xuống dòng là cố ý — mã bị bẻ dòng thì lệch thụt đầu dòng và khó
đọc hơn là phải cuộn ngang.
"""
super().__init__()
self.setObjectName("codeEditor")
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(" "))
font = QFont("Consolas")
font.setStyleHint(QFont.Monospace)
font.setPointSize(10)
self.setFont(font)
self._gutter = _LineNumbers(self)
self.blockCountChanged.connect(lambda _=0: self._update_gutter_width())
self.updateRequest.connect(self._on_update_request)
self._highlighter = PygmentsHighlighter(self.document())
self._update_gutter_width()
def line_number_width(self) -> int:
"""Bề rộng cần cho máng số dòng, tính theo số chữ số của dòng cuối cùng."""
digits = max(2, len(str(max(1, self.blockCount()))))
return 12 + self.fontMetrics().horizontalAdvance("9") * digits
def _update_gutter_width(self) -> None:
"""Chừa lề trái đúng bằng bề rộng máng số dòng."""
self.setViewportMargins(self.line_number_width(), 0, 0, 0)
def _on_update_request(self, rect, dy: int) -> None:
"""Cuộn hoặc vẽ lại vùng nào thì máng số dòng cuộn/vẽ lại đúng vùng đó."""
if dy:
self._gutter.scroll(0, dy)
else:
self._gutter.update(0, rect.y(), self._gutter.width(), rect.height())
if rect.contains(self.viewport().rect()):
self._update_gutter_width()
def resizeEvent(self, event): # noqa: N802
"""Đổi kích thước thì đặt lại hình chữ nhật của máng số dòng."""
super().resizeEvent(event)
cr = self.contentsRect()
self._gutter.setGeometry(QRect(cr.left(), cr.top(), self.line_number_width(), cr.height()))
def paint_line_numbers(self, event) -> None:
"""Vẽ số của các dòng đang hiện trên màn, bỏ qua dòng bị gập hoặc nằm ngoài khung."""
p = current_palette()
painter = QPainter(self._gutter)
painter.fillRect(event.rect(), QColor(p.code_gutter_bg))
block = self.firstVisibleBlock()
num = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
painter.setPen(QColor(p.code_gutter_fg))
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
painter.drawText(0, int(top), self._gutter.width() - 6,
self.fontMetrics().height(), Qt.AlignRight,
str(num + 1))
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
num += 1
def load_file(self, path: str, text: str) -> None:
"""Nạp nội dung tệp vào ô soạn thảo và bật tô màu theo đuôi tệp."""
self.setPlainText(text)
self._highlighter.set_filename(path, text)
__all__ = ["CodeEditor", "PygmentsHighlighter"]