仓库初始化

This commit is contained in:
lwt
2026-05-22 00:16:08 +08:00
commit 95c96dce42
118 changed files with 23005 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
"""
ripgrep (rg.exe) 封装:日志 stdin 过滤、目录内文件搜索。
"""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
from typing import List, Optional, Tuple
_RG_NAMES = ("rg.exe", "rg")
def find_rg_executable() -> Optional[Path]:
"""解析 rg 可执行文件;找不到返回 None。"""
env = (os.environ.get("RG_PATH") or "").strip()
if env:
p = Path(env)
if p.is_file():
return p
if getattr(sys, "frozen", False):
base = Path(sys.executable).resolve().parent
for name in _RG_NAMES:
cand = base / name
if cand.is_file():
return cand
# resources/bin/rg.exe(开发目录)
here = Path(__file__).resolve().parent.parent
project_root = here.parent
for name in _RG_NAMES:
cand = project_root / "resources" / "bin" / name
if cand.is_file():
return cand
w = shutil.which("rg")
if w:
return Path(w)
return None
def _build_rg_regex_pattern(pattern: str, whole_word: bool) -> str:
if whole_word:
return rf"\b(?:{pattern})\b"
return pattern
def _utf8_byte_range_to_qt_positions(s: str, start_byte: int, end_byte: int) -> Tuple[int, int]:
"""
将 UTF-8 字节区间 [start_byte, end_byte) 转为 QTextCursor 使用的 UTF-16 偏移(与 Qt 一致)。
"""
b = s.encode("utf-8")
if start_byte < 0 or end_byte > len(b) or start_byte > end_byte:
raise ValueError("invalid utf-8 byte range for document")
prefix_start = b[:start_byte].decode("utf-8")
prefix_end = b[:end_byte].decode("utf-8")
qt_start = len(prefix_start.encode("utf-16-le")) // 2
qt_end = len(prefix_end.encode("utf-16-le")) // 2
return qt_start, qt_end
def rg_match_spans_qt(
full_text: str,
pattern: str,
case_sensitive: bool,
use_regex: bool,
whole_word: bool,
max_spans: int = 3000,
) -> Tuple[Optional[List[Tuple[int, int]]], Optional[str]]:
"""
使用 ``rg --json`` 得到与 ripgrep 完全一致的匹配区间,并转为 Qt 文档坐标(UTF-16)。
返回 (spans, error)。error 非空表示未调用 rg 或失败;spans 为 ``(start, end)`` 列表,按从左到右顺序。
"""
exe = find_rg_executable()
if not exe:
return None, "未找到 rg.exe"
q = (pattern or "").strip()
if not q:
return [], None
args: List[str] = [str(exe), "--json", "--color", "never"]
if not case_sensitive:
args.append("-i")
if not use_regex:
args.append("--fixed-strings")
if whole_word:
args.append("-w")
pat_arg = q
else:
pat_arg = _build_rg_regex_pattern(q, whole_word)
args.extend(["--", pat_arg, "-"])
try:
r = subprocess.run(
args,
input=full_text.encode("utf-8"),
capture_output=True,
timeout=120,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)
if sys.platform == "win32"
else 0,
)
except Exception as e:
return None, str(e)
err_txt = (r.stderr or b"").decode("utf-8", errors="replace").strip()
if r.returncode == 2:
return None, err_txt or "rg 正则解析失败"
if r.returncode not in (0, 1):
return None, err_txt or f"rg 退出码 {r.returncode}"
out = (r.stdout or b"").decode("utf-8", errors="replace")
spans: List[Tuple[int, int]] = []
for line in out.splitlines():
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if obj.get("type") != "match":
continue
data = obj.get("data") or {}
abs_off = data.get("absolute_offset")
if abs_off is None:
continue
for sm in data.get("submatches") or []:
try:
sb = abs_off + int(sm["start"])
eb = abs_off + int(sm["end"])
qt_s, qt_e = _utf8_byte_range_to_qt_positions(full_text, sb, eb)
except (KeyError, TypeError, ValueError):
continue
spans.append((qt_s, qt_e))
if len(spans) >= max_spans:
return spans, None
return spans, None
def compile_python_span_pattern(
pattern: str,
case_sensitive: bool,
use_regex: bool,
whole_word: bool,
) -> Optional[re.Pattern]:
"""与界面选项一致;在 rg 不可用时用于高亮与跳转(Python re)。"""
if not (pattern or "").strip():
return None
q = pattern.strip()
flags = 0 if case_sensitive else re.IGNORECASE
try:
if use_regex:
src = _build_rg_regex_pattern(q, whole_word)
return re.compile(src, flags)
inner = re.escape(q)
if whole_word:
inner = rf"\b{inner}\b"
return re.compile(inner, flags)
except re.error:
return None
def rg_filter_lines(
full_text: str,
pattern: str,
case_sensitive: bool,
use_regex: bool,
whole_word: bool,
) -> Tuple[Optional[str], Optional[str]]:
"""
将全文经 stdin 交给 rg,返回仅匹配行(与 rg 默认输出一致)。
- use_regex=False``--fixed-strings``,可选 ``-w`` 全词。
- use_regex=TrueRust 正则语法(与 Python 略有差异);全词时包 ``\\b(?:... )\\b``。
返回 (result, error)error 非空表示未调用 rg 或失败。
"""
exe = find_rg_executable()
if not exe:
return None, "未找到 rg.exe,请将 rg.exe 放在 Server Manager 目录或设置环境变量 RG_PATH"
q = (pattern or "").strip()
if not q:
return full_text, None
args: List[str] = [str(exe), "--color", "never", "--no-heading"]
if not case_sensitive:
args.append("-i")
if not use_regex:
args.append("--fixed-strings")
if whole_word:
args.append("-w")
pat_arg = q
else:
pat_arg = _build_rg_regex_pattern(q, whole_word)
args.extend(["--", pat_arg, "-"])
try:
r = subprocess.run(
args,
input=full_text.encode("utf-8"),
capture_output=True,
timeout=120,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)
if sys.platform == "win32"
else 0,
)
except Exception as e:
return None, str(e)
if r.returncode not in (0, 1):
err = (r.stderr or b"").decode("utf-8", errors="replace").strip()
return None, err or f"rg 退出码 {r.returncode}"
out = (r.stdout or b"").decode("utf-8", errors="replace")
if r.returncode == 1:
return "", None
return out.rstrip("\n"), None
def rg_search_directory(
search_root: Path,
pattern: str,
case_sensitive: bool,
use_regex: bool,
whole_word: bool,
glob_globs: Optional[List[str]] = None,
) -> Tuple[str, Optional[str]]:
"""在目录下递归搜索,返回 rg 标准输出(含文件名与行号)。"""
exe = find_rg_executable()
if not exe:
return "", "未找到 rg.exe"
q = (pattern or "").strip()
if not q:
return "", "请输入搜索内容"
if not search_root.is_dir():
return "", f"目录不存在: {search_root}"
args: List[str] = [str(exe), "-n", "-S", "--color", "never", "--heading"]
if not case_sensitive:
args.append("-i")
if not use_regex:
args.append("--fixed-strings")
if whole_word:
args.append("-w")
pat_arg = q
else:
pat_arg = _build_rg_regex_pattern(q, whole_word)
if glob_globs:
for g in glob_globs:
args.extend(["--glob", g])
args.extend(["--", pat_arg, str(search_root.resolve())])
try:
r = subprocess.run(
args,
capture_output=True,
text=True,
timeout=180,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)
if sys.platform == "win32"
else 0,
)
except Exception as e:
return "", str(e)
out = (r.stdout or "").strip()
err = (r.stderr or "").strip()
if r.returncode not in (0, 1):
return "", err or f"rg 退出码 {r.returncode}"
if r.returncode == 1 and not out:
return "(无匹配)", None
return out, None