Datenstruktur des Notiz-Baums #7

Closed
opened 2026-08-27 12:33:42 +00:00 by RDietermann · 1 comment
Owner

Part of #1

Question

Welche Datenstruktur trägt den Notiz-Baum, und wie entsteht sie?

Entschieden ist: rekursiv einlesen, als aufklappbarer Baum darstellen. Offen ist alles darunter:

  • Welcher Typ? Verschachtelte Struct mit Vec<Node>-Kindern, oder eine flache Arena mit Indizes? Der Baum muss in Dioxus-Props leben, also Clone + PartialEq sein — was das an Kosten bedeutet, gehört zur Entscheidung.
  • Wo lebt der Aufklapp-Zustand? Im Baum selbst (expanded: bool pro Knoten) oder daneben (ein HashSet<PfadBuf> der offenen Ordner)? Das eine vermischt Daten und UI-Zustand, das andere hält sie getrennt.
  • Was wird eingesammelt: nur .md, oder alle Dateien mit .md hervorgehoben? Werden Ordner ohne jede .md-Datei angezeigt?
  • Was passiert mit .dionotes/ selbst und mit versteckten Ordnern wie .git?
  • Symlinks: folgen oder ignorieren? Was verhindert eine Endlosschleife?

Zielgröße: die Typdefinition, plus die Regel, welche Einträge im Baum landen.

Part of #1 ## Question Welche Datenstruktur trägt den Notiz-Baum, und wie entsteht sie? Entschieden ist: rekursiv einlesen, als aufklappbarer Baum darstellen. Offen ist alles darunter: - Welcher Typ? Verschachtelte Struct mit `Vec<Node>`-Kindern, oder eine flache Arena mit Indizes? Der Baum muss in Dioxus-Props leben, also `Clone + PartialEq` sein — was das an Kosten bedeutet, gehört zur Entscheidung. - Wo lebt der Aufklapp-Zustand? Im Baum selbst (`expanded: bool` pro Knoten) oder daneben (ein `HashSet<PfadBuf>` der offenen Ordner)? Das eine vermischt Daten und UI-Zustand, das andere hält sie getrennt. - Was wird eingesammelt: nur `.md`, oder alle Dateien mit `.md` hervorgehoben? Werden Ordner ohne jede `.md`-Datei angezeigt? - Was passiert mit `.dionotes/` selbst und mit versteckten Ordnern wie `.git`? - Symlinks: folgen oder ignorieren? Was verhindert eine Endlosschleife? Zielgröße: die Typdefinition, plus die Regel, welche Einträge im Baum landen.
RDietermann added the wayfinder:grilling label 2026-08-27 12:33:42 +00:00
RDietermann self-assigned this 2026-08-28 06:11:45 +00:00
Author
Owner

Antwort

Der Typ

#[derive(Clone, PartialEq)]
pub enum TreeNode {
    File   { name: String, path: PathBuf },
    Folder { name: String, path: PathBuf, children: Vec<TreeNode> },
}

Verschachtelt statt Arena: Der Baum wird beim Öffnen einmal aufgebaut und danach nur gelesen — die Indirektion einer Arena zahlt sich erst aus, wenn Knoten wandern oder sich gegenseitig referenzieren. Als Enum statt als Struct mit children: Vec<_>, weil damit unmöglich wird, was ein Struct erlaubt: eine Datei mit Kindern.

Eine File-Variante für alle Dateien, keine Sonderbehandlung für .md. Wer wissen will, ob es eine Notiz ist, fragt path.extension() — eine Zeile an der Stelle, die es braucht, statt einer Klassifizierung, die der Baum beim Aufbau festschreibt. Damit muss heute auch nicht entschieden werden, was außer .md als Notiz gilt.

Die Einlese-Regeln

Regel Entscheidung
Dateitypen Alle Dateien, nicht nur .md
Leere Ordner Werden angezeigt
Namen mit führendem . Übersprungen — Dateien wie Ordner
Symlinks Übersprungen
Sortierung Ordner zuerst, dann Dateien, je alphabetisch ohne Beachtung der Groß-/Kleinschreibung

Die Punkt-Regel gilt bewusst auch für Verzeichnisse: Sie erledigt .dionotes/ und .git/ in einem Aufwasch, ohne eine Ausnahmeliste, die bei .obsidian/ oder .vscode/ wieder wachsen müsste. Ein .git/ im Baum wäre nicht nur unübersichtlich — es sind schnell Tausende Objektdateien, die das Einlesen beim Öffnen spürbar verzögern.

Symlinks werden über symlink_metadata() erkannt (das folgt dem Link nicht, metadata() schon) und übersprungen. Ihnen zu folgen hieße, einen Zyklusschutz über kanonisierte Pfade mitzuführen — realer Aufwand für einen Fall, den in einem Notizordner kaum jemand hat, während ein Hänger beim Öffnen für den Nutzer schwer zu deuten wäre.

Case-insensitive sortiert vor allem deshalb, weil Notizen/ und notizen/ sonst je nach Plattform an ganz verschiedenen Stellen stehen — unter Windows sind sie ohnehin derselbe Ordner.

Der Aufbau

std::fs::read_dir mit eigener Rekursion. walkdir 2.5.0 liegt zwar bereits transitiv im Lockfile, iteriert aber flach — daraus einen Baum zusammenzusetzen ist mehr Code, nicht weniger.

Der Aufklapp-Zustand

Liegt neben dem Baum, nicht darin: Signal<HashSet<PathBuf>> der offenen Ordner.

Der Grund ist die Refresh-Entscheidung aus dem Charting: Es gibt keinen Filewatcher, der Nutzer liest bei Bedarf neu ein. Steckte expanded: bool im Knoten, klappte jeder Refresh sämtliche Ordner zu — der UI-Zustand würde mit den Daten weggeworfen, die er gar nicht beschreibt. So überlebt er den Neuaufbau ohne Übertragungslogik.

Nicht persistiert. Beim Öffnen ist die oberste Ebene sichtbar, alle Unterordner sind zu. Andernfalls schriebe die App bei jedem Auf- und Zuklappen auf die Platte, für einen Zustand, den der Nutzer in zwei Klicks wiederherstellt — und es wäre das erste Feld im Marker, das nicht aus einer Entscheidung stammt, sondern aus Bequemlichkeit. Nachrüstbar als ein Feld mit #[serde(default)].

Wo der Baum lebt — abgeleitet, nicht separat entschieden

Der Baum gehört nicht in struct Workspace, sondern in ein eigenes Signal im Context. Ein Refresh ersetzt den Baum, ohne dass sich am Workspace etwas ändert; läge er im Workspace, zöge jede Baum-Änderung dessen Vergleich nach sich. Der Workspace beschreibt, welcher Ordner offen ist — der Baum ist daraus abgeleiteter Zustand.

Widerspruch willkommen; das folgt aus #5, war aber nicht Teil der Fragerunde.

Bekannte Grenzen

  • Tiefe Bäume. Sowohl der rekursive Aufbau als auch das rekursive Drop eines verschachtelten Baums laufen über den Stack. Bei realistischen Verzeichnistiefen unkritisch, aber es ist die Stelle, die bei einem pathologischen Workspace zuerst aufgibt.
  • PartialEq auf dem Baum. Bekommt eine rekursive Komponente je einen TreeNode als Prop, vergleicht Dioxus bei jedem Rendern den ganzen Teilbaum. Fällt bei großen Workspaces zusammen mit dem Einlesen ins Gewicht — beides gehört zum Fog-Punkt „Große Workspaces" auf der Map, nicht hierher.
## Antwort ### Der Typ ```rust #[derive(Clone, PartialEq)] pub enum TreeNode { File { name: String, path: PathBuf }, Folder { name: String, path: PathBuf, children: Vec<TreeNode> }, } ``` Verschachtelt statt Arena: Der Baum wird beim Öffnen einmal aufgebaut und danach nur gelesen — die Indirektion einer Arena zahlt sich erst aus, wenn Knoten wandern oder sich gegenseitig referenzieren. Als Enum statt als Struct mit `children: Vec<_>`, weil damit unmöglich wird, was ein Struct erlaubt: eine Datei mit Kindern. **Eine `File`-Variante für alle Dateien**, keine Sonderbehandlung für `.md`. Wer wissen will, ob es eine Notiz ist, fragt `path.extension()` — eine Zeile an der Stelle, die es braucht, statt einer Klassifizierung, die der Baum beim Aufbau festschreibt. Damit muss heute auch nicht entschieden werden, was außer `.md` als Notiz gilt. ### Die Einlese-Regeln | Regel | Entscheidung | | --- | --- | | Dateitypen | **Alle** Dateien, nicht nur `.md` | | Leere Ordner | Werden **angezeigt** | | Namen mit führendem `.` | **Übersprungen — Dateien wie Ordner** | | Symlinks | **Übersprungen** | | Sortierung | Ordner zuerst, dann Dateien, je alphabetisch ohne Beachtung der Groß-/Kleinschreibung | Die Punkt-Regel gilt bewusst auch für Verzeichnisse: Sie erledigt `.dionotes/` und `.git/` in einem Aufwasch, ohne eine Ausnahmeliste, die bei `.obsidian/` oder `.vscode/` wieder wachsen müsste. Ein `.git/` im Baum wäre nicht nur unübersichtlich — es sind schnell Tausende Objektdateien, die das Einlesen beim Öffnen spürbar verzögern. Symlinks werden über `symlink_metadata()` erkannt (das folgt dem Link nicht, `metadata()` schon) und übersprungen. Ihnen zu folgen hieße, einen Zyklusschutz über kanonisierte Pfade mitzuführen — realer Aufwand für einen Fall, den in einem Notizordner kaum jemand hat, während ein Hänger beim Öffnen für den Nutzer schwer zu deuten wäre. Case-insensitive sortiert vor allem deshalb, weil `Notizen/` und `notizen/` sonst je nach Plattform an ganz verschiedenen Stellen stehen — unter Windows sind sie ohnehin derselbe Ordner. ### Der Aufbau `std::fs::read_dir` mit eigener Rekursion. `walkdir 2.5.0` liegt zwar bereits transitiv im Lockfile, iteriert aber flach — daraus einen Baum zusammenzusetzen ist mehr Code, nicht weniger. ### Der Aufklapp-Zustand Liegt **neben** dem Baum, nicht darin: `Signal<HashSet<PathBuf>>` der offenen Ordner. Der Grund ist die Refresh-Entscheidung aus dem Charting: Es gibt keinen Filewatcher, der Nutzer liest bei Bedarf neu ein. Steckte `expanded: bool` im Knoten, klappte jeder Refresh sämtliche Ordner zu — der UI-Zustand würde mit den Daten weggeworfen, die er gar nicht beschreibt. So überlebt er den Neuaufbau ohne Übertragungslogik. **Nicht persistiert.** Beim Öffnen ist die oberste Ebene sichtbar, alle Unterordner sind zu. Andernfalls schriebe die App bei jedem Auf- und Zuklappen auf die Platte, für einen Zustand, den der Nutzer in zwei Klicks wiederherstellt — und es wäre das erste Feld im Marker, das nicht aus einer Entscheidung stammt, sondern aus Bequemlichkeit. Nachrüstbar als ein Feld mit `#[serde(default)]`. ### Wo der Baum lebt — abgeleitet, nicht separat entschieden Der Baum gehört **nicht** in `struct Workspace`, sondern in ein eigenes Signal im Context. Ein Refresh ersetzt den Baum, ohne dass sich am Workspace etwas ändert; läge er im Workspace, zöge jede Baum-Änderung dessen Vergleich nach sich. Der Workspace beschreibt, *welcher* Ordner offen ist — der Baum ist daraus abgeleiteter Zustand. Widerspruch willkommen; das folgt aus #5, war aber nicht Teil der Fragerunde. ### Bekannte Grenzen - **Tiefe Bäume.** Sowohl der rekursive Aufbau als auch das rekursive `Drop` eines verschachtelten Baums laufen über den Stack. Bei realistischen Verzeichnistiefen unkritisch, aber es ist die Stelle, die bei einem pathologischen Workspace zuerst aufgibt. - **`PartialEq` auf dem Baum.** Bekommt eine rekursive Komponente je einen `TreeNode` als Prop, vergleicht Dioxus bei jedem Rendern den ganzen Teilbaum. Fällt bei großen Workspaces zusammen mit dem Einlesen ins Gewicht — beides gehört zum Fog-Punkt „Große Workspaces" auf der Map, nicht hierher.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/dionotes-client#7