# 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 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 toks = split_path_tokens(full_path) if not toks: return cur_dict = self._root_children cur_full = '' parent_node: Optional[PathNode] = None for i, tok in enumerate(toks): # Собираем ПОЛНЫЙ путь if cur_full == '': cur_full = tok else: if tok.startswith('['): cur_full += tok else: cur_full += '.' + tok # Если узел уже есть node = cur_dict.get(tok) if node is None: # --- ВАЖНО: full_path = cur_full --- 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