refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
//! `nahual_database_explorer` — explorer de SQLite.
|
||||
//!
|
||||
//! Mismo patrón que `nahual_file_explorer` pero con `SqliteProvider`. La
|
||||
//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
|
||||
//! origen de los datos: filas de una tabla `items(id, parent_id, name,
|
||||
//! display_type, content)` en lugar del filesystem.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
Context, Entity, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*,
|
||||
px,
|
||||
};
|
||||
|
||||
use nahual_core::{DataProvider, DisplayType, EntityNode};
|
||||
use nahual_provider_sqlite::SqliteDataProvider;
|
||||
use nahual_theme::Theme;
|
||||
use nahual_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
|
||||
pub enum DatabaseExplorerEvent {
|
||||
EntitySelected { id: String },
|
||||
EntityOpened { id: String },
|
||||
}
|
||||
|
||||
pub struct DatabaseExplorer {
|
||||
tree_view: Entity<TreeView>,
|
||||
provider: Arc<SqliteDataProvider>,
|
||||
db_path: String,
|
||||
|
||||
expanded: HashSet<String>,
|
||||
children: HashMap<String, Vec<EntityNode>>,
|
||||
pending: HashSet<String>,
|
||||
/// Mensaje de error si la DB no abrió. Se muestra en el header.
|
||||
open_error: Option<String>,
|
||||
}
|
||||
|
||||
const ROOT_KEY: &str = "__db_root__";
|
||||
|
||||
impl EventEmitter<DatabaseExplorerEvent> for DatabaseExplorer {}
|
||||
|
||||
impl DatabaseExplorer {
|
||||
/// `db_path` es la ruta al .sqlite. Si no existe se crea con la tabla
|
||||
/// `items` y un seed mínimo (ver `SqliteDataProvider::new`).
|
||||
pub fn new(db_path: String, cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||
|
||||
let tree_view = cx.new(|cx| TreeView::new("db-explorer-tree", cx));
|
||||
cx.subscribe(&tree_view, |this: &mut DatabaseExplorer, _, ev, cx| {
|
||||
this.on_tree_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
let (provider, open_error) = match SqliteDataProvider::new(&db_path) {
|
||||
Ok(p) => (Some(Arc::new(p)), None),
|
||||
Err(e) => (None, Some(e)),
|
||||
};
|
||||
|
||||
let mut expanded = HashSet::new();
|
||||
expanded.insert(ROOT_KEY.to_string());
|
||||
|
||||
let mut me = Self {
|
||||
tree_view,
|
||||
// Usamos un dummy provider si la DB no abrió. La UI mostrará el
|
||||
// error en el header; cualquier load_children retornará vacío.
|
||||
provider: provider.unwrap_or_else(|| {
|
||||
Arc::new(SqliteDataProvider::new(":memory:").expect("memory db"))
|
||||
}),
|
||||
db_path,
|
||||
expanded,
|
||||
children: HashMap::new(),
|
||||
pending: HashSet::new(),
|
||||
open_error,
|
||||
};
|
||||
// Cargar el root (parent_id NULL en SQLite, mapped to None acá).
|
||||
me.load_children(ROOT_KEY.to_string(), cx);
|
||||
me
|
||||
}
|
||||
|
||||
pub fn db_path(&self) -> &str {
|
||||
&self.db_path
|
||||
}
|
||||
|
||||
fn load_children(&mut self, parent_key: String, cx: &mut Context<Self>) {
|
||||
if self.pending.contains(&parent_key) || self.children.contains_key(&parent_key) {
|
||||
return;
|
||||
}
|
||||
self.pending.insert(parent_key.clone());
|
||||
|
||||
let provider = self.provider.clone();
|
||||
let parent_for_task = parent_key.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
// ROOT_KEY → None; cualquier otro → Some(actual id).
|
||||
let arg: Option<String> = if parent_for_task == ROOT_KEY {
|
||||
None
|
||||
} else {
|
||||
Some(parent_for_task.clone())
|
||||
};
|
||||
let result = provider.list_children(arg.as_deref()).await;
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.on_children_loaded(parent_for_task, result, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_children_loaded(
|
||||
&mut self,
|
||||
parent_key: String,
|
||||
result: Result<Vec<EntityNode>, String>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.pending.remove(&parent_key);
|
||||
let mut entries = result.unwrap_or_default();
|
||||
sort_entries(&mut entries);
|
||||
self.children.insert(parent_key, entries);
|
||||
self.push_rows(cx);
|
||||
}
|
||||
|
||||
fn push_rows(&self, cx: &mut Context<Self>) {
|
||||
let mut rows = Vec::new();
|
||||
// Una row "raíz virtual" para que el árbol tenga un anchor visible.
|
||||
rows.push(TreeRow {
|
||||
id: RowId::new(ROOT_KEY),
|
||||
label: format!("(db) {}", self.db_path),
|
||||
depth: 0,
|
||||
kind: RowKind::Branch,
|
||||
expanded: self.expanded.contains(ROOT_KEY),
|
||||
icon: Some("🗄️".to_string()),
|
||||
});
|
||||
if self.expanded.contains(ROOT_KEY) {
|
||||
self.append_children(ROOT_KEY, 1, &mut rows);
|
||||
}
|
||||
|
||||
self.tree_view
|
||||
.update(&mut *cx, |tree, cx| tree.set_rows(rows, cx));
|
||||
}
|
||||
|
||||
fn append_children(&self, parent: &str, depth: u32, out: &mut Vec<TreeRow>) {
|
||||
let Some(children) = self.children.get(parent) else { return };
|
||||
for entry in children {
|
||||
let kind = match entry.display_type {
|
||||
DisplayType::Folder => RowKind::Branch,
|
||||
_ => RowKind::Leaf,
|
||||
};
|
||||
let icon = match entry.display_type {
|
||||
DisplayType::Folder => "📂",
|
||||
DisplayType::File => "📄",
|
||||
DisplayType::Stream => "📡",
|
||||
};
|
||||
let is_expanded = self.expanded.contains(&entry.id);
|
||||
out.push(TreeRow {
|
||||
id: RowId::new(entry.id.clone()),
|
||||
label: entry.name.clone(),
|
||||
depth,
|
||||
kind,
|
||||
expanded: is_expanded,
|
||||
icon: Some(icon.to_string()),
|
||||
});
|
||||
if is_expanded {
|
||||
self.append_children(&entry.id, depth + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context<Self>) {
|
||||
match event {
|
||||
TreeEvent::ChevronToggled(id) => {
|
||||
let key = id.as_str().to_string();
|
||||
if !self.expanded.remove(&key) {
|
||||
self.expanded.insert(key.clone());
|
||||
self.load_children(key, cx);
|
||||
}
|
||||
self.push_rows(cx);
|
||||
}
|
||||
TreeEvent::RowClicked(id) => {
|
||||
let key = id.as_str();
|
||||
if key == ROOT_KEY {
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.find_entry(key) {
|
||||
if !matches!(entry.display_type, DisplayType::Folder) {
|
||||
cx.emit(DatabaseExplorerEvent::EntitySelected {
|
||||
id: key.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TreeEvent::RowDoubleClicked(id) => {
|
||||
let key = id.as_str();
|
||||
if key == ROOT_KEY {
|
||||
return;
|
||||
}
|
||||
if let Some(entry) = self.find_entry(key) {
|
||||
if !matches!(entry.display_type, DisplayType::Folder) {
|
||||
cx.emit(DatabaseExplorerEvent::EntityOpened {
|
||||
id: key.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TreeEvent::ContextMenuRequested { .. } | TreeEvent::ActiveChanged(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_entry(&self, id: &str) -> Option<&EntityNode> {
|
||||
for entries in self.children.values() {
|
||||
if let Some(e) = entries.iter().find(|e| e.id == id) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DatabaseExplorer {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
let pending_count = self.pending.len();
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_panel.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.h(px(28.0))
|
||||
.px(px(8.0))
|
||||
.border_b_1()
|
||||
.border_color(theme.border)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap(px(6.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child("🗄️"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.fg_text)
|
||||
.child(SharedString::from(self.db_path.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.ml_auto()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(if pending_count > 0 {
|
||||
format!("⏳ {}", pending_count)
|
||||
} else {
|
||||
String::new()
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(if let Some(err) = self.open_error.clone() {
|
||||
// Si la DB no abrió, mostramos el error y no pintamos el
|
||||
// árbol vacío — sería confuso.
|
||||
div()
|
||||
.p(px(12.0))
|
||||
.text_size(px(11.0))
|
||||
.text_color(theme.accent_strong)
|
||||
.child(SharedString::from(format!("error abriendo DB: {}", err)))
|
||||
} else {
|
||||
div().flex_grow().min_h(px(0.0)).child(self.tree_view.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_entries(entries: &mut Vec<EntityNode>) {
|
||||
entries.sort_by(|a, b| {
|
||||
let a_dir = matches!(a.display_type, DisplayType::Folder);
|
||||
let b_dir = matches!(b.display_type, DisplayType::Folder);
|
||||
b_dir
|
||||
.cmp(&a_dir)
|
||||
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user