velxio/backend/app/api/routes/intellisense.py

426 lines
17 KiB
Python
Raw Normal View History

"""Intellisense symbol server.
Serves editor symbol tables (classes, public methods, constants, enum values)
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
for the Arduino libraries a host actually has. Extraction is done by the
dependency-free scanner in app.services.symbol_extract, which also mirrors
the pro overlay's cache naming rules; this module never imports app.pro.
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
Two library layouts answer this endpoint, in that order:
1. The content-addressed library cache (VELXIO_LIBCACHE_DIR, default
/var/velxio/libcache — entry dirs named `<norm_name>@<norm_version>-
<sha12>`). Immutable by construction, so a scan cached under an entry
name can never go stale and the response is safe to cache hard.
2. A plain arduino-cli sketchbook `libraries/` dir — what Velxio Desktop's
sidecar and a self-host have. Entry dirs are named after the library and
the version lives in library.properties, and they are MUTABLE (`lib
upgrade` rewrites one in place), so scans are keyed by a cheap content
stamp instead of by the dir name.
A host with neither simply 404s and the editor degrades to the engine's
embedded catalog.
"""
from __future__ import annotations
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
import hashlib
import json
import logging
import os
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
import re
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
import subprocess
import tempfile
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
import time
from pathlib import Path
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
from typing import NamedTuple, Optional
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
from app.services.symbol_extract import (
_entry_version_key,
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
_read_props,
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
find_cache_entry,
norm_name,
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
norm_version,
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
parse_spec,
scan_library,
)
logger = logging.getLogger(__name__)
router = APIRouter()
# Entry dir names are content-addressed (sha12 suffix), so the payload for a
# given spec resolution can be cached hard by browsers/proxies.
_CACHE_HEADERS = {"Cache-Control": "public, max-age=86400"}
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
class _Source(NamedTuple):
"""One library layout this endpoint can answer from. `content_addressed`
is the whole difference: it decides how an entry is found, how its scan is
keyed, and where the scan cache is written."""
root: Path
content_addressed: bool
def _cache_root() -> Path:
"""Resolved per-request so tests (and container reconfigs) can point the
endpoint elsewhere via the environment."""
return Path(os.environ.get("VELXIO_LIBCACHE_DIR", "/var/velxio/libcache"))
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
# arduino-cli's own sketchbook, asked once and remembered: on the desktop
# sidecar nobody sets an env var for it, and guessing ~/Arduino is wrong on
# the platforms where the default is ~/Documents/Arduino. Whatever arduino-cli
# answers is by definition the dir the compile resolves libraries from.
_CLI_PROBE_TTL_S = 600
_cli_probe: tuple[float, Optional[Path]] = (0.0, None)
def _cli_sketchbook() -> Optional[Path]:
global _cli_probe
ts, cached = _cli_probe
now = time.time()
if ts and now - ts < _CLI_PROBE_TTL_S:
return cached
found: Optional[Path] = None
try:
out = subprocess.run(
["arduino-cli", "config", "get", "directories.user"],
capture_output=True, text=True, timeout=5,
)
if out.returncode == 0:
p = Path(out.stdout.strip().strip('"'))
if p.is_dir():
found = p
except (OSError, subprocess.SubprocessError):
pass # no arduino-cli on PATH (web image is fine — it has the cache)
_cli_probe = (now, found)
return found
def _sketchbook_libraries() -> Optional[Path]:
"""The `libraries/` dir of the local sketchbook, or None.
VELXIO_SYMBOL_LIBRARIES_DIR names the libraries dir itself (the explicit
override, for a host that keeps its libraries somewhere unusual); the
sketchbook env vars name its parent; arduino-cli is asked last but knows
best. The home-dir guesses are only reached when arduino-cli cannot be
run at all.
"""
direct = os.environ.get("VELXIO_SYMBOL_LIBRARIES_DIR")
if direct and Path(direct).is_dir():
return Path(direct)
for env in ("ARDUINO_DIRECTORIES_USER", "VELXIO_FALLBACK_SKETCHBOOK"):
val = os.environ.get(env)
if val and (Path(val) / "libraries").is_dir():
return Path(val) / "libraries"
probed = _cli_sketchbook()
if probed is not None and (probed / "libraries").is_dir():
return probed / "libraries"
home = Path.home()
for rel in ("Arduino/libraries", "Documents/Arduino/libraries"):
if (home / rel).is_dir():
return home / rel
return None
def _sources() -> list[_Source]:
"""The layouts to try, best first. The sketchbook is skipped when it
resolves to the cache itself: the pro overlay points arduino-cli's
sketchbook at `<cache>/../cache-sketchbook/libraries`, a symlink to the
cache root, and scanning it twice under two shapes buys nothing."""
out: list[_Source] = []
cache = _cache_root()
if cache.is_dir():
out.append(_Source(cache, True))
libs = _sketchbook_libraries()
if libs is not None:
try:
same = any(s.root.resolve() == libs.resolve() for s in out)
except OSError:
same = False
if not same:
out.append(_Source(libs, False))
return out
def _stamp(entry: Path) -> str:
"""Content fingerprint of a MUTABLE library dir: file count, total size and
newest mtime over the tree, stat only — no reads. Changes whenever
arduino-cli installs or upgrades the library, which is what lets a scan be
cached on disk without ever going stale."""
newest = 0.0
total = 0
count = 0
for parent, dirs, files in os.walk(entry):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
try:
st = os.stat(os.path.join(parent, f))
except OSError:
continue
count += 1
total += st.st_size
newest = max(newest, st.st_mtime)
raw = f"{count}:{total}:{newest:.6f}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
def _entry_key(src: _Source, entry: Path) -> str:
"""Cache key for one entry's scan. A content-addressed entry IS its key;
a sketchbook folder gets the same `<norm_name>@<norm_version>-<12 hex>`
shape built from its properties plus the content stamp."""
if src.content_addressed:
return entry.name
props = _read_props(entry)
name = norm_name(props.get("name", "")) or norm_name(entry.name) or "lib"
return f"{name}@{norm_version(props.get('version', ''))}-{_stamp(entry)}"
def _symbols_dir(src: _Source) -> Path:
"""Where scans are persisted. Inside the cache root for the cache; beside
the sketchbook's `libraries/` for a sketchbook, so nothing Velxio writes
ever lands where arduino-cli looks for libraries."""
if src.content_addressed:
return src.root / ".symbols"
return src.root.parent / ".velxio-symbols"
def _entries(src: _Source) -> list[Path]:
try:
return sorted(
d for d in src.root.iterdir() if d.is_dir() and not d.name.startswith(".")
)
except OSError:
return []
def _find_entry(src: _Source, name: str, version: Optional[str]) -> Optional[Path]:
if src.content_addressed:
return find_cache_entry(src.root, name, version)
# A sketchbook folder is matched on library.properties `name=` (or the
# folder name), and the version is a PREFERENCE, not a filter: the local
# build compiles against whatever is installed, so answering with the
# installed version beats 404ing because the project pinned another one.
want = norm_name(name)
if not want:
return None
fallback: Optional[Path] = None
for d in _entries(src):
props = _read_props(d)
if norm_name(props.get("name", "")) != want and norm_name(d.name) != want:
continue
if version is None or norm_version(props.get("version", "")) == norm_version(version):
return d
if fallback is None:
fallback = d
return fallback
def _write_symbol_cache(sym_file: Path, payload: dict) -> None:
"""Atomically persist a scan result (tmp file + rename). Failures are
logged and ignored — the response was already computed."""
try:
sym_file.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=str(sym_file.parent), prefix=".tmp-", suffix=".json")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(payload, f)
os.replace(tmp, sym_file)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
except OSError as e:
logger.warning("[intellisense] symbol cache write failed for %s: %s", sym_file, e)
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
def _payload_for(src: _Source, entry: Path) -> dict:
"""Cached scan of one entry (the same disk cache get_symbols uses)."""
sym_file = _symbols_dir(src) / f"{_entry_key(src, entry)}.json"
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
if sym_file.is_file():
try:
return json.loads(sym_file.read_text(encoding="utf-8"))
except (OSError, ValueError):
pass
payload = scan_library(entry)
_write_symbol_cache(sym_file, payload)
return payload
_TYPE_INDEX_NAME = "_types.json"
_TYPE_INDEX_MAX_AGE_S = 6 * 3600
feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators Measured against the user corpus, the by-resolved-type long tail was losing what the old catch-all happened to include: for `Adafruit_SH1106G display(...); display.setTextSize(` the class itself declares one method (begin) and everything people call lives four levels up (Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv, Adafruit_NeoMatrix and every GFX-derived display driver. Three changes: - The scanner records `bases` on class symbols (access/virtual keywords dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the production cache now carry their bases. - /type/{name} walks the chain breadth-first through the type index (bases routinely live in a different library), re-owns inherited members to the requested type so the editor sees one flat class, tags them with `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes from 1 member to 64. - `void a(int), b(), c(char);` declares three methods; the extractor took only the first. TFT_eSPI declares setTextColor/setTextSize and setCursor/getCursorX/... this way. Split on top-level commas after a complete `name(args)` group. TFT_eSPI 118 -> 173 members. Rescan of the 1,125-entry production cache: +274 member symbols, 15 libraries gain, none lose.
2026-08-18 00:08:57 +07:00
_MAX_BASES = 8
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
def _index_key(src: _Source, entries: list[Path]) -> str:
"""Fingerprint of the whole source. Entry keys already move whenever any
library's bytes change, so an index built under this key describes exactly
the libraries present."""
joined = ",".join(_entry_key(src, e) for e in entries)
return hashlib.sha256(joined.encode("utf-8")).hexdigest()[:12]
def _type_index(src: _Source) -> dict[str, str]:
"""owner-class name -> entry dir name, over the whole source. Built by
scanning every entry once (each scan is itself disk-cached), persisted
next to the scans and rebuilt when the source's fingerprint moves or the
index ages out. When two entries declare the same class (a library and
its fork, or two versions), the newest entry for the alphabetically-first
library wins -- deterministic and, for the common case of one library per
class, irrelevant."""
idx_file = _symbols_dir(src) / _TYPE_INDEX_NAME
entries = _entries(src)
key = _index_key(src, entries)
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
if idx_file.is_file():
try:
cached = json.loads(idx_file.read_text(encoding="utf-8"))
fresh = (time.time() - idx_file.stat().st_mtime) < _TYPE_INDEX_MAX_AGE_S
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
if fresh and cached.get("index_key") == key:
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
return cached["types"]
except (OSError, ValueError, KeyError):
pass
# Many classes are declared by more than one cached library: the real
# one plus forks and bundles that vendor a copy (PubSubClient ships inside
# "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station",
# AccelStepper inside "Arduino Learning Board"). Pick, per class:
# 1. the library whose normalized name equals the class name, else
# 2. the library declaring the MOST members of that class (the
# canonical copy is the complete one; vendored copies are trimmed),
# 3. ties: alphabetical, deterministic.
best: dict[str, tuple[int, int, str]] = {} # owner -> (name_match, member_count, entry) with a sortable key
by_name: dict[str, list[Path]] = {}
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
for entry in entries:
# Cache entries carry the version in the dir name, so several map to
# one library; a sketchbook holds one dir per library already.
by_name.setdefault(entry.name.partition("@")[0], []).append(entry)
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
for lib_name in sorted(by_name):
newest = sorted(by_name[lib_name], key=_entry_version_key, reverse=True)[0]
try:
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
payload = _payload_for(src, newest)
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
except Exception as e: # noqa: BLE001 - one bad library must not kill the index
logger.warning("[intellisense] type index: skipping %s: %s", newest.name, e)
continue
counts: dict[str, int] = {}
for sym in payload.get("symbols", []):
owner = sym.get("owner")
if owner:
counts[owner] = counts.get(owner, 0) + 1
for owner, n in counts.items():
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
name_match = 1 if norm_name(owner) == norm_name(lib_name) else 0
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
candidate = (name_match, n, newest.name)
cur = best.get(owner)
# higher name_match, then higher count, then (already alphabetical
# by iteration order) keep the first
if cur is None or (candidate[0], candidate[1]) > (cur[0], cur[1]):
best[owner] = candidate
types = {owner: entry for owner, (_, _, entry) in best.items()}
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
_write_symbol_cache(idx_file, {"index_key": key, "types": types})
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
return types
@router.get("/type/{name}")
def get_type_members(name: str) -> JSONResponse:
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
"""Members of one class/struct, whichever library declares it.
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
This is the long-tail path that is safe for completion quality: the
engine calls it only after tree-sitter has resolved a receiver to a
declared type that the embedded catalog does not know (`PubSubClient
client(espClient); client.` -> PubSubClient), and it returns ONLY that
type's members -- never a library's module-level constants or macros,
which are what made feeding whole library sets into the candidate pool
measurably worse. Payload shape matches /symbols/ so the engine's
RemoteSymbolCache can store it unchanged."""
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
sources = _sources()
if not sources:
raise HTTPException(status_code=404, detail="no library source available")
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
if not re.fullmatch(r"[A-Za-z_]\w{0,80}", name):
raise HTTPException(status_code=400, detail="not a type name")
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
for src in sources:
types = _type_index(src)
entry_name = types.get(name)
if entry_name is None:
continue
entry = src.root / entry_name
if not entry.is_dir():
continue # stale index for this source; try the next
payload = _payload_for(src, entry)
break
else:
raise HTTPException(status_code=404, detail=f"no installed library declares {name}")
feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators Measured against the user corpus, the by-resolved-type long tail was losing what the old catch-all happened to include: for `Adafruit_SH1106G display(...); display.setTextSize(` the class itself declares one method (begin) and everything people call lives four levels up (Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv, Adafruit_NeoMatrix and every GFX-derived display driver. Three changes: - The scanner records `bases` on class symbols (access/virtual keywords dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the production cache now carry their bases. - /type/{name} walks the chain breadth-first through the type index (bases routinely live in a different library), re-owns inherited members to the requested type so the editor sees one flat class, tags them with `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes from 1 member to 64. - `void a(int), b(), c(char);` declares three methods; the extractor took only the first. TFT_eSPI declares setTextColor/setTextSize and setCursor/getCursorX/... this way. Split on top-level commas after a complete `name(args)` group. TFT_eSPI 118 -> 173 members. Rescan of the 1,125-entry production cache: +274 member symbols, 15 libraries gain, none lose.
2026-08-18 00:08:57 +07:00
# Own members first, then the inheritance chain, breadth-first, each base
# resolved through the same index (it may live in another library:
# Adafruit_SH1106G -> Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX
# crosses two). Members are re-owned to the requested type so the editor
# sees one flat class; `bases` lists the chain so a host that already
# embeds one of them (Adafruit_GFX is curated client-side) can dedupe.
symbols: list[dict] = []
seen_names: set[str] = set()
chain: list[str] = []
queue = [(name, payload)]
visited = {name}
while queue and len(chain) < _MAX_BASES:
cur, cur_payload = queue.pop(0)
if cur != name:
chain.append(cur)
for sym in cur_payload.get("symbols", []):
if sym.get("owner") != cur or sym["name"] in seen_names:
continue
seen_names.add(sym["name"])
out = dict(sym)
out["owner"] = name
if cur != name:
out["inheritedFrom"] = cur
symbols.append(out)
cls = next(
(s for s in cur_payload.get("symbols", []) if s.get("kind") == "class" and s.get("name") == cur),
None,
)
for base in (cls or {}).get("bases", []):
if base in visited:
continue
visited.add(base)
base_entry = types.get(base)
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
if base_entry and (src.root / base_entry).is_dir():
queue.append((base, _payload_for(src, src.root / base_entry)))
feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators Measured against the user corpus, the by-resolved-type long tail was losing what the old catch-all happened to include: for `Adafruit_SH1106G display(...); display.setTextSize(` the class itself declares one method (begin) and everything people call lives four levels up (Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv, Adafruit_NeoMatrix and every GFX-derived display driver. Three changes: - The scanner records `bases` on class symbols (access/virtual keywords dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the production cache now carry their bases. - /type/{name} walks the chain breadth-first through the type index (bases routinely live in a different library), re-owns inherited members to the requested type so the editor sees one flat class, tags them with `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes from 1 member to 64. - `void a(int), b(), c(char);` declares three methods; the extractor took only the first. TFT_eSPI declares setTextColor/setTextSize and setCursor/getCursorX/... this way. Split on top-level commas after a complete `name(args)` group. TFT_eSPI 118 -> 173 members. Rescan of the 1,125-entry production cache: +274 member symbols, 15 libraries gain, none lose.
2026-08-18 00:08:57 +07:00
else:
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
# Not in this source (a core class like Print/Stream, or an
feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators Measured against the user corpus, the by-resolved-type long tail was losing what the old catch-all happened to include: for `Adafruit_SH1106G display(...); display.setTextSize(` the class itself declares one method (begin) and everything people call lives four levels up (Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv, Adafruit_NeoMatrix and every GFX-derived display driver. Three changes: - The scanner records `bases` on class symbols (access/virtual keywords dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the production cache now carry their bases. - /type/{name} walks the chain breadth-first through the type index (bases routinely live in a different library), re-owns inherited members to the requested type so the editor sees one flat class, tags them with `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes from 1 member to 64. - `void a(int), b(), c(char);` declares three methods; the extractor took only the first. TFT_eSPI declares setTextColor/setTextSize and setCursor/getCursorX/... this way. Split on top-level commas after a complete `name(args)` group. TFT_eSPI 118 -> 173 members. Rescan of the 1,125-entry production cache: +274 member symbols, 15 libraries gain, none lose.
2026-08-18 00:08:57 +07:00
# embedded one like Adafruit_GFX): still report it so the
# host can supply the members itself.
chain.append(base)
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
return JSONResponse(
content={
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
"id": payload.get("id") or entry.name,
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
"triggers": payload.get("triggers", []),
"type": name,
feat(intellisense): /type/ follows inheritance; extractor records bases and splits comma declarators Measured against the user corpus, the by-resolved-type long tail was losing what the old catch-all happened to include: for `Adafruit_SH1106G display(...); display.setTextSize(` the class itself declares one method (begin) and everything people call lives four levels up (Adafruit_SH110X -> Adafruit_GrayOLED -> Adafruit_GFX -> Print). Same story for MCUFRIEND_kbv, Adafruit_NeoMatrix and every GFX-derived display driver. Three changes: - The scanner records `bases` on class symbols (access/virtual keywords dropped, `ns::Base` -> Base, template args cut). 2,529 classes across the production cache now carry their bases. - /type/{name} walks the chain breadth-first through the type index (bases routinely live in a different library), re-owns inherited members to the requested type so the editor sees one flat class, tags them with `inheritedFrom`, and lists the chain in `bases` -- including bases NOT in the cache (Print, Stream, or an embedded one like Adafruit_GFX) so the host can supply those itself. Depth capped at 8. Adafruit_SH1106G goes from 1 member to 64. - `void a(int), b(), c(char);` declares three methods; the extractor took only the first. TFT_eSPI declares setTextColor/setTextSize and setCursor/getCursorX/... this way. Split on top-level commas after a complete `name(args)` group. TFT_eSPI 118 -> 173 members. Rescan of the 1,125-entry production cache: +274 member symbols, 15 libraries gain, none lose.
2026-08-18 00:08:57 +07:00
"bases": chain,
"symbols": symbols,
feat(intellisense): GET /api/intellisense/type/{name} — members of one class The completion-safe long-tail path. Feeding whole library symbol sets into the editor's candidate pool was measured to make ranking worse (U8g2 alone injects ~250 module-level class names; ArduinoJson dozens of ARDUINOJSON_* macros). What does help is answering `client.` once tree-sitter has resolved `client` to a declared type the embedded catalog does not know -- and for that the engine needs "the members of type T", not "the symbols of library L". This endpoint returns exactly that: symbols whose owner is the requested class, from whichever cached library declares it, in the same payload shape as /symbols/ so the engine's RemoteSymbolCache stores it unchanged. Resolution: a class-name -> cache-entry index built once over the library cache (each per-entry scan is already disk-cached), persisted under .symbols/_types.json (3,818 classes / 199 KB over the 1,125-entry production cache; ~1 s to rebuild from cached scans, ~60 s cold), refreshed after a few hours or when the cache grows. Many classes are declared by more than one library -- the real one plus forks and bundles vendoring a copy (PubSubClient inside "ESP8266 Microgear", NTPClient inside "ESP8266 Weather Station", AccelStepper inside "Arduino Learning Board") -- so the pick is: the library named after the class, else the one declaring the most members of it (vendored copies are trimmed), else alphabetical. Generic, not pro-specific: any host with a library cache can use it.
2026-08-17 23:51:08 +07:00
},
headers=_CACHE_HEADERS,
)
@router.get("/symbols/{spec}")
def get_symbols(spec: str) -> JSONResponse:
"""Symbol table for one library. `spec` is `Name@Version` or a bare
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
`Name` (which resolves to the newest version present). Names and versions
are normalized with the same rules the cache uses, so `Adafruit GFX
Library@1.12.6` and `adafruitgfxlibrary@1.12.6` hit the same entry."""
name, version = parse_spec(spec)
feat(intellisense): answer from the local sketchbook, not only the cache The symbol server assumed one library layout: the content-addressed cache, where an entry dir name IS its content hash. A host without that cache -- Velxio Desktop's sidecar, and any self-host -- has its libraries where arduino-cli put them, under plain folder names with the version inside library.properties, and got a 404 for every request. The editor there falls back to the engine's embedded catalog, so `client.` on a library the user installed himself resolves to nothing. Both layouts are now sources, tried best-first. The only real difference is mutability: `lib upgrade` rewrites a sketchbook folder in place and keeps its name, so a scan cannot be keyed by that name. Sketchbook entries are keyed by a cheap stat-only stamp (file count, total size, newest mtime) in the same `<norm_name>@<norm_version>-<12 hex>` shape, which makes the disk cache exact rather than merely long-lived. Cache entries keep their existing keys, so no deployed scan is invalidated. Where the sketchbook is: an explicit override, then the sketchbook env vars, then arduino-cli itself (`config get directories.user`, probed once and remembered) -- guessing ~/Arduino is wrong on the platforms whose default is ~/Documents/Arduino. A sketchbook that resolves to the cache is dropped: the pro overlay points arduino-cli at a symlink to the cache root, and scanning it twice under two shapes buys nothing. Scans for a sketchbook are written to `<sketchbook>/.velxio-symbols/`, beside `libraries/` and never inside it, so nothing Velxio writes shows up in `arduino-cli lib list`. The type index now keys on a fingerprint of the source's entry keys instead of the entry count, which is exact for both layouts (an in-place upgrade changes no count). Deployed indexes rebuild once, ~1s from the cached scans.
2026-08-25 05:06:05 +07:00
sources = _sources()
if not sources:
raise HTTPException(status_code=404, detail="no library source available")
for src in sources:
entry = _find_entry(src, name, version)
if entry is not None:
return JSONResponse(content=_payload_for(src, entry), headers=_CACHE_HEADERS)
raise HTTPException(status_code=404, detail=f"library not installed: {spec}")