仓库初始化
This commit is contained in:
@@ -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=True:Rust 正则语法(与 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
|
||||
Reference in New Issue
Block a user