debugVarTool/Src/path_hints.py
Razvalyaev 502046091c опять кууууча всего:
базово доделаны терминалки до более менее итогового состояния
2025-07-23 17:13:28 +03:00

320 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# path_hints.py
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import re
# ---------------------- tokenization helpers ----------------------
def split_path_tokens(path: str) -> List[str]:
"""
Разбивает строку пути на логические части:
'foo[2].bar[1]->baz' -> ['foo', '[2]', 'bar', '[1]', 'baz']
Аналог твоей split_path(), но оставлена как чистая функция.
"""
tokens: List[str] = []
token = ''
i = 0
L = len(path)
while i < L:
c = path[i]
# '->'
if c == '-' and i + 1 < L and path[i:i+2] == '->':
if token:
tokens.append(token)
token = ''
i += 2
continue
# одиночный '-' в конце
if c == '-' and i == L - 1:
i += 1
continue
# '.'
if c == '.':
if token:
tokens.append(token)
token = ''
i += 1
continue
# '[' ... ']'
if c == '[':
if token:
tokens.append(token)
token = ''
idx = ''
while i < L and path[i] != ']':
idx += path[i]
i += 1
if i < L and path[i] == ']':
idx += ']'
i += 1
tokens.append(idx)
continue
# обычный символ
token += c
i += 1
if token:
tokens.append(token)
return tokens
def split_path_tokens_with_spans(path: str) -> List[Tuple[str, int, int]]:
"""
Возвращает список кортежей (токен, start_pos, end_pos)
Токены — так же, как в split_path_tokens, но с позициями в исходной строке.
"""
tokens = []
i = 0
L = len(path)
while i < L:
c = path[i]
start = i
# '->'
if c == '-' and i + 1 < L and path[i:i+2] == '->':
tokens.append(('->', start, start + 2))
i += 2
continue
if c == '.':
tokens.append(('.', start, start + 1))
i += 1
continue
if c == '[':
# захватим весь индекс с ']'
j = i
while j < L and path[j] != ']':
j += 1
if j < L and path[j] == ']':
j += 1
tokens.append((path[i:j], i, j))
i = j
continue
# иначе - обычное имя (до точки, стрелки или скобок)
j = i
while j < L and path[j] not in ['.', '-', '[']:
if path[j] == '-' and j + 1 < L and path[j:j+2] == '->':
break
j += 1
tokens.append((path[i:j], i, j))
i = j
# фильтруем из списка токены-разделители '.' и '->' чтобы оставить только логические части
filtered = [t for t in tokens if t[0] not in ['.', '->']]
return filtered
def canonical_key(path: str) -> str:
"""
Преобразует путь к канонической форме для индекса / поиска:
- '->' -> '.'
- '[' -> '.['
- lower()
"""
p = path.replace('->', '.')
p = p.replace('[', '.[')
return p.lower()
# ---------------------- индекс узлов ----------------------
@dataclass
class PathNode:
"""
Узел в логическом дереве путей.
Храним:
- собственное имя (локальное, напр. 'controller' или '[3]')
- полный путь (оригинальный, как его должен видеть пользователь)
- тип (опционально; widget может хранить отдельно)
- дети
"""
name: str
full_path: str
type_str: str = ''
children: Dict[str, "PathNode"] = field(default_factory=dict)
def add_child(self, child: "PathNode") -> None:
self.children[child.name] = child
def get_children(self) -> List["PathNode"]:
"""
Вернуть список дочерних узлов, отсортированных по имени.
"""
return sorted(self.children.values(), key=lambda n: n.name)
class PathHints:
"""
Движок автоподсказок / completion.
Работает с плоским списком ПОЛНЫХ имён (как показываются пользователю).
Сам восстанавливает иерархию и выдаёт подсказки по текущему вводу.
Qt-независим.
"""
def __init__(self) -> None:
self._paths: List[str] = []
self._types: Dict[str, str] = {} # full_path -> type_str (опционально)
self._index: Dict[str, PathNode] = {} # canonical full path -> node
self._root_children: Dict[str, PathNode] = {} # top-level по первому токену
# ------------ Подаём данные ------------
def set_paths(self,
paths: List[Tuple[str, Optional[str]]]
) -> None:
"""
paths: список кортежей (full_path, type_str|None).
Пример: ('project.controller.read.errors.bit.status_er0', 'unsigned int')
Поля могут содержать '->' и индексы, т.е. строки в пользовательском формате.
NOTE: порядок не важен; дерево строится автоматически.
"""
self._paths = []
self._types.clear()
self._index.clear()
self._root_children.clear()
for p, t in paths:
if t is None:
t = ''
self._add_path(p, t)
def _add_path(self, full_path: str, type_str: str) -> None:
self._paths.append(full_path)
self._types[full_path] = type_str
tokens_spans = split_path_tokens_with_spans(full_path)
if not tokens_spans:
return
cur_dict = self._root_children
cur_full = ''
parent_node: Optional[PathNode] = None
for i, (tok, start, end) in enumerate(tokens_spans):
cur_full = full_path[:end] # подстрока с начала до конца токена включительно
node = cur_dict.get(tok)
if node is None:
node = PathNode(name=tok, full_path=cur_full)
cur_dict[tok] = node
# Регистрируем все узлы, включая промежуточные
self._index[canonical_key(cur_full)] = node
parent_node = node
cur_dict = node.children
# В последний узел добавляем тип
if parent_node:
parent_node.type_str = type_str
# ------------ Поиск узла ------------
def find_node(self, path: str) -> Optional[PathNode]:
return self._index.get(canonical_key(path))
def get_children(self, full_path: str) -> List[PathNode]:
"""
Вернуть список дочерних узлов PathNode для заданного полного пути.
Если узел не найден — вернуть пустой список.
"""
node = self.find_node(full_path)
if node is None:
return []
return node.get_children()
# ------------ Подсказки ------------
def suggest(self,
text: str,
*,
include_partial: bool = True
) -> List[str]:
"""
Вернёт список *полных имён узлов*, подходящих под ввод.
Правила (упрощённо, повторяя твою update_completions()):
- Если текст пуст → top-level.
- Если заканчивается на '.' или '->' или '[' → вернуть детей текущего узла.
- Иначе → фильтр по последнему фрагменту (prefix substring match).
"""
text = text or ''
stripped = text.strip()
# пусто: top-level
if stripped == '':
return sorted(self._root_full_names())
# Завершение по разделителю?
if stripped.endswith('.') or stripped.endswith('->') or stripped.endswith('['):
base = stripped[:-1] if stripped.endswith('[') else stripped.rstrip('.').rstrip('>').rstrip('-')
node = self.find_node(base)
if node:
return self._children_full_names(node)
# не нашли базу — ничего
return []
# иначе: обычный поиск по последней части
toks = split_path_tokens(stripped)
prefix_last = toks[-1].lower() if toks else ''
parent_toks = toks[:-1]
if not parent_toks:
# фильтр top-level
res = []
for name, node in self._root_children.items():
if prefix_last == '' or prefix_last in name.lower():
res.append(node.full_path)
return sorted(res)
# есть родитель
parent_path = self._join_tokens(parent_toks)
parent_node = self.find_node(parent_path)
if not parent_node:
return []
res = []
for child in parent_node.children.values():
if prefix_last == '' or prefix_last in child.name.lower():
res.append(child.full_path)
return sorted(res)
def add_separator(self, full_path: str) -> str:
"""
Возвращает full_path с добавленным разделителем ('.' или '['),
если у узла есть дети и пользователь ещё не поставил разделитель.
Если первый ребёнок — массивный токен ('[0]') → добавляем '['.
Позже можно допилить '->' для указателей.
"""
node = self.find_node(full_path)
text = full_path
if node and node.children and not (
text.endswith('.') or text.endswith('->') or text.endswith('[')
):
first_child = next(iter(node.children.values()))
if first_child.name.startswith('['):
text += '[' # сразу начинаем индекс
else:
text += '.' # обычный переход
return text
# ------------ внутренние вспомогательные ------------
def _root_full_names(self) -> List[str]:
return [node.full_path for node in self._root_children.values()]
def _children_full_names(self, node: PathNode) -> List[str]:
return [ch.full_path for ch in node.children.values()]
@staticmethod
def _join_tokens(tokens: List[str]) -> str:
"""
Собираем путь обратно. Для внутренних нужд (поиск), формат не критичен —
всё равно canonical_key() нормализует.
"""
if not tokens:
return ''
out = tokens[0]
for t in tokens[1:]:
if t.startswith('['):
out += t
else:
out += '.' + t
return out