mod connection;
mod dialogs;
mod scripts;
mod tree;
pub use connection::*;
pub use dialogs::*;
pub use scripts::*;
pub use tree::*;
use std::collections::HashMap;
use crate::core::models::*;
use crate::ui::tabs::{TabId, TabKind, WorkspaceTab};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Normal,
Insert,
Visual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Sidebar,
ScriptsPanel,
TabContent,
}
#[derive(Debug, Clone)]
pub struct TabGroup {
pub tab_ids: Vec<TabId>,
pub active_idx: usize,
}
impl TabGroup {
pub fn new(tab_ids: Vec<TabId>, active_idx: usize) -> Self {
Self {
tab_ids,
active_idx,
}
}
pub fn active_tab_id(&self) -> Option<TabId> {
self.tab_ids.get(self.active_idx).copied()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Overlay {
ConnectionDialog,
ObjectFilter,
ConnectionMenu,
GroupMenu,
Help,
ConfirmClose,
ConfirmQuit,
SaveScriptName,
ScriptConnection,
ThemePicker,
BindVariables,
SaveGridChanges,
ConfirmDeleteConnection { name: String },
ConfirmDropObject,
RenameObject,
ConfirmCompile,
ExportDialog,
ImportDialog,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OilPane {
Explorer,
Scripts,
}
pub struct OilState {
pub pane: OilPane,
pub previous_focus: Focus,
}
impl OilState {
pub fn new(previous_focus: Focus) -> Self {
Self {
pane: OilPane::Explorer,
previous_focus,
}
}
}
pub struct LeaderState {
pub pending: bool,
pub b_pending: bool,
pub w_pending: bool,
pub s_pending: bool,
pub f_pending: bool,
pub q_pending: bool,
pub leader_pending: bool,
pub pressed_at: Option<std::time::Instant>,
pub help_visible: bool,
}
impl LeaderState {
pub fn new() -> Self {
Self {
pending: false,
b_pending: false,
w_pending: false,
s_pending: false,
f_pending: false,
q_pending: false,
leader_pending: false,
pressed_at: None,
help_visible: false,
}
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.pending = false;
self.b_pending = false;
self.w_pending = false;
self.s_pending = false;
self.f_pending = false;
self.q_pending = false;
self.leader_pending = false;
self.pressed_at = None;
self.help_visible = false;
}
}
pub struct EngineState {
pub completion: Option<crate::ui::completion::CompletionState>,
pub diagnostics: Vec<crate::ui::diagnostics::Diagnostic>,
pub last_diagnostic_run: Option<std::time::Instant>,
pub column_cache: HashMap<String, Vec<Column>>,
pub metadata_indexes: HashMap<String, crate::sql_engine::metadata::MetadataIndex>,
pub diagnostic_hover: Option<(usize, String)>,
pub diagnostic_list_visible: bool,
pub diagnostic_list_cursor: usize,
pub server_diag_generation: u64,
pub last_server_diag_dispatch: Option<std::time::Instant>,
pub pending_server_diag: Option<(String, String)>,
pub analysis_cache: Option<AnalysisCache>,
}
pub struct AnalysisCache {
pub block_lines: Vec<String>,
pub cursor_row: usize,
pub cursor_col: usize,
pub context: crate::sql_engine::context::SemanticContext,
}
impl EngineState {
pub fn new() -> Self {
Self {
completion: None,
diagnostics: vec![],
last_diagnostic_run: None,
column_cache: HashMap::new(),
metadata_indexes: HashMap::new(),
diagnostic_hover: None,
diagnostic_list_visible: false,
diagnostic_list_cursor: 0,
server_diag_generation: 0,
last_server_diag_dispatch: None,
pending_server_diag: None,
analysis_cache: None,
}
}
}
pub struct AppState {
pub mode: Mode,
pub focus: Focus,
pub overlay: Option<Overlay>,
pub tabs: Vec<WorkspaceTab>,
pub active_tab_idx: usize,
pub next_tab_id: u64,
pub groups: Option<[TabGroup; 2]>,
pub active_group: usize,
pub rendering_group: Option<usize>,
pub conn: ConnectionState,
pub sidebar: SidebarState,
pub sidebar_visible: bool,
pub oil: Option<OilState>,
pub status_message: String,
pub loading: bool,
pub loading_since: Option<std::time::Instant>,
pub pending_d: bool,
pub metadata_ready: bool,
pub compile_confirmed: bool,
pub dialogs: DialogState,
pub leader: LeaderState,
pub scripts: ScriptsState,
pub engine: EngineState,
#[allow(dead_code)]
pub bindings: crate::keybindings::KeyBindings,
}
impl AppState {
pub fn new() -> Self {
Self {
mode: Mode::Normal,
focus: Focus::TabContent,
overlay: None,
tabs: vec![],
active_tab_idx: 0,
next_tab_id: 1,
groups: None,
active_group: 0,
rendering_group: None,
conn: ConnectionState::new(),
sidebar: SidebarState::new(),
sidebar_visible: false,
oil: None,
status_message: "Ready - press 'a' to add connection, '?' for help".to_string(),
loading: false,
loading_since: None,
pending_d: false,
metadata_ready: false,
compile_confirmed: false,
dialogs: DialogState::new(),
leader: LeaderState::new(),
scripts: ScriptsState::new(),
engine: EngineState::new(),
bindings: crate::keybindings::KeyBindings::defaults(),
}
}
pub fn active_tab(&self) -> Option<&WorkspaceTab> {
self.tabs.get(self.active_tab_idx)
}
pub fn active_tab_mut(&mut self) -> Option<&mut WorkspaceTab> {
self.tabs.get_mut(self.active_tab_idx)
}
#[allow(dead_code)]
pub fn focused_group_tab_ids(&self) -> Vec<TabId> {
match &self.groups {
Some(groups) => groups[self.active_group].tab_ids.clone(),
None => self.tabs.iter().map(|t| t.id).collect(),
}
}
pub fn focused_tab_id(&self) -> Option<TabId> {
match &self.groups {
Some(groups) => groups[self.active_group].active_tab_id(),
None => self.tabs.get(self.active_tab_idx).map(|t| t.id),
}
}
pub fn create_empty_split(&mut self) {
if self.groups.is_some() {
self.active_group = 1;
return;
}
let all_ids: Vec<TabId> = self.tabs.iter().map(|t| t.id).collect();
let g0 = TabGroup::new(all_ids, self.active_tab_idx);
let g1 = TabGroup::new(Vec::new(), 0);
self.groups = Some([g0, g1]);
self.active_group = 1;
}
pub fn sync_active_tab_idx(&mut self) {
if let Some(focused_id) = self.focused_tab_id()
&& let Some(idx) = self.tabs.iter().position(|t| t.id == focused_id)
{
self.active_tab_idx = idx;
}
}
pub fn find_tab(&self, id: TabId) -> Option<&WorkspaceTab> {
self.tabs.iter().find(|t| t.id == id)
}
pub fn available_groups(&self) -> Vec<String> {
let mut groups = Vec::new();
for node in &self.sidebar.tree {
if let TreeNode::Group { name, .. } = node
&& !groups.contains(name)
{
groups.push(name.clone());
}
}
if groups.is_empty() {
groups.push("Default".to_string());
}
groups
}
pub fn find_tab_mut(&mut self, id: TabId) -> Option<&mut WorkspaceTab> {
self.tabs.iter_mut().find(|t| t.id == id)
}
pub fn alloc_tab_id(&mut self) -> TabId {
let id = TabId(self.next_tab_id);
self.next_tab_id += 1;
id
}
pub fn open_or_focus_tab(&mut self, kind: TabKind) -> TabId {
let candidate_tab = if let Some(groups) = &self.groups {
let focused_ids = &groups[self.active_group].tab_ids;
self.tabs
.iter()
.position(|t| focused_ids.contains(&t.id) && t.kind.same_object(&kind))
} else {
self.tabs.iter().position(|t| t.kind.same_object(&kind))
};
if let Some(idx) = candidate_tab {
let existing_id = self.tabs[idx].id;
self.active_tab_idx = idx;
self.focus = Focus::TabContent;
if let Some(groups) = self.groups.as_mut()
&& let Some(pos) = groups[self.active_group]
.tab_ids
.iter()
.position(|id| *id == existing_id)
{
groups[self.active_group].active_idx = pos;
}
return existing_id;
}
let id = self.alloc_tab_id();
let tab = match &kind {
TabKind::Script {
file_path,
name,
conn_name,
} => WorkspaceTab::new_script(id, name.clone(), file_path.clone(), conn_name.clone()),
TabKind::Table {
conn_name,
schema,
table,
} => WorkspaceTab::new_table(id, conn_name.clone(), schema.clone(), table.clone()),
TabKind::Package {
conn_name,
schema,
name,
} => WorkspaceTab::new_package(id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Function {
conn_name,
schema,
name,
} => WorkspaceTab::new_function(id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Procedure {
conn_name,
schema,
name,
} => WorkspaceTab::new_procedure(id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::DbType {
conn_name,
schema,
name,
} => WorkspaceTab::new_db_type(id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Trigger {
conn_name,
schema,
name,
} => WorkspaceTab::new_trigger(id, conn_name.clone(), schema.clone(), name.clone()),
};
self.tabs.push(tab);
self.active_tab_idx = self.tabs.len() - 1;
self.focus = Focus::TabContent;
if let Some(groups) = self.groups.as_mut() {
groups[self.active_group].tab_ids.push(id);
groups[self.active_group].active_idx = groups[self.active_group].tab_ids.len() - 1;
}
id
}
pub fn close_active_tab(&mut self) {
if self.tabs.is_empty() {
return;
}
let closing_id = match &self.groups {
Some(groups) => match groups[self.active_group].active_tab_id() {
Some(id) => id,
None => return,
},
None => self.tabs[self.active_tab_idx].id,
};
if let Some(groups) = self.groups.as_mut() {
let g = &mut groups[self.active_group];
if let Some(pos) = g.tab_ids.iter().position(|id| *id == closing_id) {
g.tab_ids.remove(pos);
if g.active_idx >= g.tab_ids.len() && !g.tab_ids.is_empty() {
g.active_idx = g.tab_ids.len() - 1;
}
}
let other = 1 - self.active_group;
let still_referenced = groups[other].tab_ids.contains(&closing_id);
let focused_empty = groups[self.active_group].tab_ids.is_empty();
if focused_empty {
let surviving = groups[other].clone();
self.groups = None;
self.active_group = 0;
self.reorder_tabs_to_group(&surviving);
if !still_referenced
&& let Some(idx) = self.tabs.iter().position(|t| t.id == closing_id)
{
self.tabs.remove(idx);
}
self.sync_active_tab_idx();
if self.tabs.is_empty() {
self.active_tab_idx = 0;
self.focus = Focus::Sidebar;
}
} else {
if !still_referenced
&& let Some(idx) = self.tabs.iter().position(|t| t.id == closing_id)
{
self.tabs.remove(idx);
}
self.sync_active_tab_idx();
}
return;
}
if let Some(idx) = self.tabs.iter().position(|t| t.id == closing_id) {
self.tabs.remove(idx);
}
if self.tabs.is_empty() {
self.active_tab_idx = 0;
self.focus = Focus::Sidebar;
} else if self.active_tab_idx >= self.tabs.len() {
self.active_tab_idx = self.tabs.len() - 1;
}
}
fn reorder_tabs_to_group(&mut self, group: &TabGroup) {
let mut new_tabs: Vec<WorkspaceTab> = Vec::with_capacity(group.tab_ids.len());
for id in &group.tab_ids {
if let Some(pos) = self.tabs.iter().position(|t| t.id == *id) {
new_tabs.push(self.tabs.remove(pos));
}
}
new_tabs.append(&mut self.tabs);
self.tabs = new_tabs;
self.active_tab_idx = group.active_idx.min(self.tabs.len().saturating_sub(1));
}
pub fn visible_tree(&self) -> Vec<(usize, &TreeNode, &str)> {
let mut visible = Vec::with_capacity(self.sidebar.tree.len());
let mut i = 0;
let mut current_conn: &str = "";
let mut key_buf = String::with_capacity(64);
while i < self.sidebar.tree.len() {
let node = &self.sidebar.tree[i];
if let TreeNode::Connection { name, .. } = node {
current_conn = name;
}
if let TreeNode::Schema { name, .. } = node {
key_buf.clear();
key_buf.push_str(current_conn);
key_buf.push_str("::schemas");
if !self.sidebar.object_filter.is_enabled(&key_buf, name) {
let d = node.depth();
i += 1;
while i < self.sidebar.tree.len() && self.sidebar.tree[i].depth() > d {
i += 1;
}
continue;
}
}
if let TreeNode::Leaf {
name, schema, kind, ..
} = node
{
let cat_suffix = match kind {
LeafKind::Table => "Tables",
LeafKind::View => "Views",
LeafKind::MaterializedView => "MaterializedViews",
LeafKind::Index => "Indexes",
LeafKind::Sequence => "Sequences",
LeafKind::Type => "Types",
LeafKind::Trigger => "Triggers",
LeafKind::Package => "Packages",
LeafKind::Procedure => "Procedures",
LeafKind::Function => "Functions",
LeafKind::Event => "Events",
};
key_buf.clear();
key_buf.push_str(current_conn);
key_buf.push_str("::");
key_buf.push_str(schema);
key_buf.push('.');
key_buf.push_str(cat_suffix);
if !self.sidebar.object_filter.is_enabled(&key_buf, name) {
i += 1;
continue;
}
}
visible.push((i, node, current_conn));
if !node.is_expanded() {
let d = node.depth();
i += 1;
while i < self.sidebar.tree.len() && self.sidebar.tree[i].depth() > d {
i += 1;
}
} else {
i += 1;
}
}
visible
}
pub fn filter_hint_for(&self, node: &TreeNode, conn_name: &str) -> Option<String> {
match node {
TreeNode::Connection { expanded: true, .. } => {
let key = format!("{conn_name}::schemas");
if self.sidebar.object_filter.has_filter(&key) {
let total = self.schema_names_for_conn(conn_name).len();
let enabled = self
.sidebar
.object_filter
.filters
.get(&key)
.map(|s| s.len())
.unwrap_or(total);
Some(format!("... ({enabled}/{total} schemas shown)"))
} else {
None
}
}
TreeNode::Category {
expanded: true,
schema,
kind,
..
} => {
let base_key = kind.filter_key(schema);
let key = format!("{conn_name}::{base_key}");
if self.sidebar.object_filter.has_filter(&key) {
let total_in_tree = self.leaves_under_category_count(&base_key);
let enabled = self
.sidebar
.object_filter
.filters
.get(&key)
.map(|s| s.len())
.unwrap_or(total_in_tree);
Some(format!("... ({enabled}/{total_in_tree} shown)"))
} else {
None
}
}
_ => None,
}
}
fn leaves_under_category_count(&self, filter_key: &str) -> usize {
let parts: Vec<&str> = filter_key.splitn(2, '.').collect();
if parts.len() != 2 {
return 0;
}
let (schema, kind_str) = (parts[0], parts[1]);
self.sidebar
.tree
.iter()
.filter(|n| {
if let TreeNode::Leaf {
schema: s, kind, ..
} = n
{
let k = format!("{:?}", kind);
s == schema && kind_str.starts_with(&k)
} else {
false
}
})
.count()
}
pub fn selected_tree_index(&self) -> Option<usize> {
let visible = self.visible_tree();
visible
.get(self.sidebar.tree_state.cursor)
.map(|(idx, _, _)| *idx)
}
pub fn connection_for_tree_idx(&self, idx: usize) -> Option<&str> {
let mut i = idx;
loop {
if let TreeNode::Connection { name, .. } = &self.sidebar.tree[i] {
return Some(name.as_str());
}
if i == 0 {
break;
}
i -= 1;
}
None
}
pub fn leaves_under_category(&self, cat_idx: usize) -> Vec<String> {
let mut items = vec![];
let cat_depth = self.sidebar.tree[cat_idx].depth();
let mut i = cat_idx + 1;
while i < self.sidebar.tree.len() && self.sidebar.tree[i].depth() > cat_depth {
if let TreeNode::Leaf { name, .. } = &self.sidebar.tree[i] {
items.push(name.clone());
}
i += 1;
}
items
}
pub fn schema_names_for_conn(&self, conn_name: &str) -> Vec<String> {
let mut in_target = false;
let mut schemas = Vec::new();
for node in &self.sidebar.tree {
match node {
TreeNode::Connection { name, .. } => {
in_target = name == conn_name;
}
TreeNode::Schema { name, .. } if in_target => {
schemas.push(name.clone());
}
_ => {}
}
}
schemas
}
pub fn all_schema_names(&self) -> Vec<String> {
self.sidebar
.tree
.iter()
.filter_map(|n| {
if let TreeNode::Schema { name, .. } = n {
Some(name.clone())
} else {
None
}
})
.collect()
}
#[allow(dead_code)]
pub fn filter_hint(&self, key: &str, total_in_tree: usize) -> Option<String> {
if let Some(set) = self.sidebar.object_filter.filters.get(key)
&& !set.is_empty()
&& set.len() < total_in_tree
{
return Some(format!("... ({}/{} filtered)", set.len(), total_in_tree));
}
None
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}