Untitled

UNIGINE guids.db parser
 avatar
unknown
python
9 months ago
6.3 kB
20
Indexable
# show_guid_tooltip_with_db.py
# Notepad++ PythonScript:
# - On hover, if pointer is over text like: guid://<hex>
# - Finds the closest "guids.db" near the currently opened file (same folder or any parent)
# - Parses it as a JSON mapping: "<hash>": "<relative/or/absolute/path>"
# - Shows the mapped filename in a Scintilla calltip; if missing, shows "GUID not found"

# Requirements: Install the free “PythonScript” plugin from Notepad++’s Plugins Admin,
# then add this as a new script and run it at startup.

import os
import re
import json
import io
from Npp import editor, notepad, SCINTILLANOTIFICATION

# ---------- Settings ----------
DWELL_MS = 500
TOKEN_RE = re.compile(r'guid://([0-9a-fA-F]+)')  # captures the GUID hex after guid://
DB_FILENAME = "guids.db"
MAX_UPWARD_STEPS = 12  # how far up the folder tree to search
# -----------------------------

# Simple cache for DB lookups to avoid re-reading on every hover
_db_cache = {
    # key: db_path -> {"mtime": float, "map": dict}
}
# Cache nearest DB per base folder (speed up path discovery)
_nearest_db_cache = {
    # key: folder_path -> db_path or None
}

def _read_text(path):
    # Try UTF-8 first, then fallback to system default
    try:
        with io.open(path, "r", encoding="utf-8") as f:
            return f.read()
    except UnicodeDecodeError:
        with io.open(path, "r", encoding="mbcs", errors="replace") as f:
            return f.read()

def _load_db(db_path):
    """Load/refresh the JSON mapping from db_path (with caching and light tolerance)."""
    try:
        mtime = os.path.getmtime(db_path)
    except Exception:
        return {}

    cached = _db_cache.get(db_path)
    if cached and cached.get("mtime") == mtime:
        return cached.get("map", {})

    raw = _read_text(db_path).strip()
    mapping = {}

    # Try strict JSON first
    try:
        mapping = json.loads(raw)
        if not isinstance(mapping, dict):
            mapping = {}
    except Exception:
        # Be tolerant with files that look like:
        #   "hash":"path",
        # Wrap with braces if it's not already
        text = raw
        if not raw.lstrip().startswith("{"):
            text = "{\n" + raw + "\n}"
        # Remove trailing commas before closing brace to be nice
        text = re.sub(r",\s*}", "}", text)
        try:
            mapping = json.loads(text)
            if not isinstance(mapping, dict):
                mapping = {}
        except Exception:
            mapping = {}

    _db_cache[db_path] = {"mtime": mtime, "map": mapping}
    return mapping

def _find_nearest_db(start_folder):
    """Find the closest guids.db: check folder, then parents up to MAX_UPWARD_STEPS."""
    if not start_folder:
        return None

    # Use cached result if available
    cached = _nearest_db_cache.get(start_folder)
    if cached is not None:
        return cached

    folder = start_folder
    steps = 0
    while folder and steps <= MAX_UPWARD_STEPS:
        candidate = os.path.join(folder, DB_FILENAME)
        if os.path.isfile(candidate):
            _nearest_db_cache[start_folder] = candidate
            return candidate
        # Move up one level
        parent = os.path.dirname(folder)
        if parent == folder:
            break
        folder = parent
        steps += 1

    _nearest_db_cache[start_folder] = None
    return None

def _match_token_at_pos(pos):
    """Return (anchor_abs_pos, guid_hex) if hover is inside a guid:// token on that line."""
    line_idx = editor.lineFromPosition(pos)
    line_start = editor.positionFromLine(line_idx)
    line_end = editor.getLineEndPosition(line_idx)
    if line_end <= line_start:
        return None

    line_text = editor.getTextRange(line_start, line_end)
    col = pos - line_start

    for m in TOKEN_RE.finditer(line_text):
        s, e = m.span()
        if s <= col < e:
            guid_hex = m.group(1).lower()
            return (line_start + s, guid_hex)
    return None

def _lookup_guid_name(guid_hex, base_folder):
    """Find nearest guids.db, load it, and return mapped filename or None."""
    db_path = _find_nearest_db(base_folder)
    if not db_path:
        return None

    db_map = _load_db(db_path)
    # keys are expected without "guid://" prefix; we captured only hex
    val = db_map.get(guid_hex)
    # Some teams store uppercase keys; try a few variants:
    if val is None:
        val = db_map.get(guid_hex.upper())
    return val

def _current_file_folder():
    try:
        fn = notepad.getCurrentFilename()
        if fn:
            return os.path.dirname(fn)
    except Exception:
        pass
    return None

def on_dwell_start(args):
    pos = args['position']
    hit = _match_token_at_pos(pos)
    if not hit:
        return
    anchor_abs, guid_hex = hit

    base_folder = _current_file_folder()
    mapped = _lookup_guid_name(guid_hex, base_folder)

    if mapped:
        tip = mapped
    else:
        # Friendly fallback message
        tip = "GUID not found in nearest '{}'".format(DB_FILENAME)

    editor.callTipShow(anchor_abs, tip)

def on_dwell_end(args):
    editor.callTipCancel()

def _reset_caches_if_file_changed():
    # When buffer changes, the "nearest db" may differ
    _nearest_db_cache.clear()
    # Keep _db_cache (so we reuse already loaded DBs), but you could also clear:
    # _db_cache.clear()

def on_buffer_activated(args):
    _reset_caches_if_file_changed()

def on_dir_changed(args):
    # If user changed working dir, nearest-db resolution might change
    _nearest_db_cache.clear()

# ----- Hook up everything -----
try:
    editor.setMouseDwellTime(DWELL_MS)
    editor.callback(on_dwell_start, [SCINTILLANOTIFICATION.DWELLSTART])
    editor.callback(on_dwell_end,   [SCINTILLANOTIFICATION.DWELLEND])
    notepad.callback(on_buffer_activated, [NOTIFICATION.BUFFERACTIVATED])
    notepad.callback(on_dir_changed,      [NOTIFICATION.DIRTYFILESCHANGED])  # benign; can be removed
except Exception:
    # Older PythonScript builds may not have every notification constant;
    # the core hover logic will still work.
    pass
Editor is loading...
Leave a Comment