use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::core::models::{Column, PackageContent, QueryResult};
use crate::core::virtual_fs::SyncState;
use vimltui::VimEditor;
use vimltui::VimModeConfig;
pub struct CellEdit {
pub col: usize,
#[allow(dead_code)]
pub original: String,
pub value: String,
}
pub enum RowChange {
Modified { edits: Vec<CellEdit> },
New { values: Vec<String> },
Deleted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TabId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubFocus {
Editor, Results, QueryView, }
pub struct ResultTab {
pub label: String,
pub result: QueryResult,
pub error_editor: Option<VimEditor>, pub query_editor: Option<VimEditor>, pub scroll_row: usize,
pub selected_row: usize,
pub selected_col: usize,
pub visible_height: usize,
pub selection_anchor: Option<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TabKind {
Script {
file_path: Option<String>,
name: String,
conn_name: Option<String>,
},
Table {
conn_name: String,
schema: String,
table: String,
},
Package {
conn_name: String,
schema: String,
name: String,
},
Function {
conn_name: String,
schema: String,
name: String,
},
Procedure {
conn_name: String,
schema: String,
name: String,
},
DbType {
conn_name: String,
schema: String,
name: String,
},
Trigger {
conn_name: String,
schema: String,
name: String,
},
}
impl TabKind {
pub fn display_name(&self) -> &str {
match self {
TabKind::Script { name, .. } => name,
TabKind::Table { table, .. } => table,
TabKind::Package { name, .. } => name,
TabKind::Function { name, .. } => name,
TabKind::Procedure { name, .. } => name,
TabKind::DbType { name, .. } => name,
TabKind::Trigger { name, .. } => name,
}
}
pub fn kind_label(&self) -> &str {
match self {
TabKind::Script { .. } => "script",
TabKind::Table { .. } => "table",
TabKind::Package { .. } => "package",
TabKind::Function { .. } => "function",
TabKind::Procedure { .. } => "procedure",
TabKind::DbType { .. } => "type",
TabKind::Trigger { .. } => "trigger",
}
}
pub fn conn_name(&self) -> Option<&str> {
match self {
TabKind::Script { conn_name, .. } => conn_name.as_deref(),
TabKind::Table { conn_name, .. } => Some(conn_name),
TabKind::Package { conn_name, .. } => Some(conn_name),
TabKind::Function { conn_name, .. } => Some(conn_name),
TabKind::Procedure { conn_name, .. } => Some(conn_name),
TabKind::DbType { conn_name, .. } => Some(conn_name),
TabKind::Trigger { conn_name, .. } => Some(conn_name),
}
}
pub fn icon(&self) -> &str {
match self {
TabKind::Script { .. } => "S",
TabKind::Table { .. } => "T",
TabKind::Package { .. } => "P",
TabKind::Function { .. } => "\u{03bb}", TabKind::Procedure { .. } => "\u{0192}", TabKind::DbType { .. } => "\u{22a4}", TabKind::Trigger { .. } => "\u{26a1}", }
}
pub fn same_object(&self, other: &TabKind) -> bool {
match (self, other) {
(
TabKind::Table {
conn_name: c1,
schema: s1,
table: t1,
},
TabKind::Table {
conn_name: c2,
schema: s2,
table: t2,
},
) => c1 == c2 && s1 == s2 && t1 == t2,
(
TabKind::Package {
conn_name: c1,
schema: s1,
name: n1,
},
TabKind::Package {
conn_name: c2,
schema: s2,
name: n2,
},
) => c1 == c2 && s1 == s2 && n1 == n2,
(
TabKind::Function {
conn_name: c1,
schema: s1,
name: n1,
},
TabKind::Function {
conn_name: c2,
schema: s2,
name: n2,
},
) => c1 == c2 && s1 == s2 && n1 == n2,
(
TabKind::Procedure {
conn_name: c1,
schema: s1,
name: n1,
},
TabKind::Procedure {
conn_name: c2,
schema: s2,
name: n2,
},
) => c1 == c2 && s1 == s2 && n1 == n2,
(
TabKind::DbType {
conn_name: c1,
schema: s1,
name: n1,
},
TabKind::DbType {
conn_name: c2,
schema: s2,
name: n2,
},
) => c1 == c2 && s1 == s2 && n1 == n2,
(
TabKind::Trigger {
conn_name: c1,
schema: s1,
name: n1,
},
TabKind::Trigger {
conn_name: c2,
schema: s2,
name: n2,
},
) => c1 == c2 && s1 == s2 && n1 == n2,
(TabKind::Script { name: n1, .. }, TabKind::Script { name: n2, .. }) => n1 == n2,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubView {
TableData,
TableProperties,
TableDDL,
PackageBody,
PackageDeclaration,
PackageFunctions,
PackageProcedures,
TypeAttributes,
TypeMethods,
TypeDeclaration,
TypeBody,
TriggerColumns,
TriggerDeclaration,
}
impl SubView {
pub fn label(&self) -> &str {
match self {
SubView::TableData => "Data",
SubView::TableProperties => "Properties",
SubView::TableDDL => "DDL",
SubView::PackageBody => "Body",
SubView::PackageDeclaration => "Declaration",
SubView::PackageFunctions => "Functions",
SubView::PackageProcedures => "Procedures",
SubView::TypeAttributes => "Attributes",
SubView::TypeMethods => "Methods",
SubView::TypeDeclaration => "Declaration",
SubView::TypeBody => "Body",
SubView::TriggerColumns => "Columns",
SubView::TriggerDeclaration => "Declaration",
}
}
}
pub struct WorkspaceTab {
pub id: TabId,
pub kind: TabKind,
pub active_sub_view: Option<SubView>,
pub query_result: Option<QueryResult>, pub table_data_result: Option<QueryResult>, pub columns: Vec<Column>,
pub result_tabs: Vec<ResultTab>, pub active_result_idx: usize, pub grid_scroll_row: usize,
pub grid_scroll_col: usize,
pub grid_selected_row: usize,
pub grid_selected_col: usize,
pub grid_visible_height: usize,
pub grid_selection_anchor: Option<(usize, usize)>, pub grid_visual_mode: bool, pub grid_on_header: bool, pub grid_focused: bool, pub streaming: bool, pub streaming_since: Option<std::time::Instant>, pub streaming_abort: Option<tokio::task::AbortHandle>, pub sub_focus: SubFocus, pub ddl_editor: Option<VimEditor>,
pub grid_error_editor: Option<VimEditor>, pub grid_query_editor: Option<VimEditor>, pub grid_changes: HashMap<usize, RowChange>, pub grid_editing: Option<(usize, usize)>, pub grid_edit_buffer: String, pub grid_edit_cursor: usize,
pub package_content: Option<PackageContent>,
pub body_editor: Option<VimEditor>,
pub decl_editor: Option<VimEditor>,
pub package_functions: Vec<String>,
pub package_procedures: Vec<String>,
pub package_list_cursor: usize,
pub type_attributes: Option<QueryResult>,
pub type_methods: Option<QueryResult>,
pub trigger_columns: Option<QueryResult>,
pub editor: Option<VimEditor>,
pub original_decl: Option<String>,
pub original_body: Option<String>,
pub original_source: Option<String>,
pub saved_content_hash: u64,
pub sync_state: Option<SyncState>,
}
impl WorkspaceTab {
pub fn new_script(
id: TabId,
name: String,
file_path: Option<String>,
conn_name: Option<String>,
) -> Self {
Self {
id,
kind: TabKind::Script {
file_path,
name,
conn_name,
},
active_sub_view: None,
editor: Some(VimEditor::new_empty(VimModeConfig::default())),
..Self::empty(id)
}
}
pub fn new_table(id: TabId, conn_name: String, schema: String, table: String) -> Self {
Self {
id,
kind: TabKind::Table {
conn_name,
schema,
table,
},
active_sub_view: Some(SubView::TableData),
ddl_editor: Some(VimEditor::new_empty(VimModeConfig::read_only())),
..Self::empty(id)
}
}
pub fn new_package(id: TabId, conn_name: String, schema: String, name: String) -> Self {
Self {
id,
kind: TabKind::Package {
conn_name,
schema,
name,
},
active_sub_view: Some(SubView::PackageDeclaration),
decl_editor: Some(VimEditor::new_empty(VimModeConfig::default())),
body_editor: Some(VimEditor::new_empty(VimModeConfig::default())),
..Self::empty(id)
}
}
pub fn new_function(id: TabId, conn_name: String, schema: String, name: String) -> Self {
Self {
id,
kind: TabKind::Function {
conn_name,
schema,
name,
},
active_sub_view: None,
editor: Some(VimEditor::new_empty(VimModeConfig::default())),
..Self::empty(id)
}
}
pub fn new_procedure(id: TabId, conn_name: String, schema: String, name: String) -> Self {
Self {
id,
kind: TabKind::Procedure {
conn_name,
schema,
name,
},
active_sub_view: None,
editor: Some(VimEditor::new_empty(VimModeConfig::default())),
..Self::empty(id)
}
}
pub fn new_db_type(id: TabId, conn_name: String, schema: String, name: String) -> Self {
Self {
id,
kind: TabKind::DbType {
conn_name,
schema,
name,
},
active_sub_view: Some(SubView::TypeAttributes),
decl_editor: Some(VimEditor::new_empty(VimModeConfig::read_only())),
body_editor: Some(VimEditor::new_empty(VimModeConfig::read_only())),
..Self::empty(id)
}
}
pub fn new_trigger(id: TabId, conn_name: String, schema: String, name: String) -> Self {
Self {
id,
kind: TabKind::Trigger {
conn_name,
schema,
name,
},
active_sub_view: Some(SubView::TriggerColumns),
decl_editor: Some(VimEditor::new_empty(VimModeConfig::read_only())),
..Self::empty(id)
}
}
pub fn clone_for_split(&self, new_id: TabId) -> Self {
let mut tab = match &self.kind {
TabKind::Script {
file_path,
name,
conn_name,
} => Self::new_script(new_id, name.clone(), file_path.clone(), conn_name.clone()),
TabKind::Table {
conn_name,
schema,
table,
} => Self::new_table(new_id, conn_name.clone(), schema.clone(), table.clone()),
TabKind::Package {
conn_name,
schema,
name,
} => Self::new_package(new_id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Function {
conn_name,
schema,
name,
} => Self::new_function(new_id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Procedure {
conn_name,
schema,
name,
} => Self::new_procedure(new_id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::DbType {
conn_name,
schema,
name,
} => Self::new_db_type(new_id, conn_name.clone(), schema.clone(), name.clone()),
TabKind::Trigger {
conn_name,
schema,
name,
} => Self::new_trigger(new_id, conn_name.clone(), schema.clone(), name.clone()),
};
if matches!(self.kind, TabKind::Script { .. })
&& let Some(src_editor) = &self.editor
&& let Some(dst_editor) = tab.editor.as_mut()
{
dst_editor.set_content(&src_editor.content());
dst_editor.modified = src_editor.modified;
}
tab
}
fn empty(id: TabId) -> Self {
Self {
id,
kind: TabKind::Script {
file_path: None,
name: String::new(),
conn_name: None,
},
active_sub_view: None,
query_result: None,
table_data_result: None,
columns: Vec::new(),
result_tabs: Vec::new(),
active_result_idx: 0,
grid_scroll_row: 0,
grid_scroll_col: 0,
grid_selected_row: 0,
grid_selected_col: 0,
grid_visible_height: 20,
grid_selection_anchor: None,
grid_visual_mode: false,
grid_on_header: false,
grid_focused: false,
streaming: false,
streaming_since: None,
streaming_abort: None,
sub_focus: SubFocus::Editor,
ddl_editor: None,
grid_error_editor: None,
grid_query_editor: None,
grid_changes: HashMap::new(),
grid_editing: None,
grid_edit_buffer: String::new(),
grid_edit_cursor: 0,
package_content: None,
body_editor: None,
decl_editor: None,
package_functions: Vec::new(),
package_procedures: Vec::new(),
package_list_cursor: 0,
type_attributes: None,
type_methods: None,
trigger_columns: None,
editor: None,
original_decl: None,
original_body: None,
original_source: None,
saved_content_hash: 0,
sync_state: None,
}
}
pub fn available_sub_views(&self) -> Vec<SubView> {
match &self.kind {
TabKind::Table { .. } => vec![
SubView::TableData,
SubView::TableProperties,
SubView::TableDDL,
],
TabKind::Package { .. } => vec![
SubView::PackageDeclaration,
SubView::PackageBody,
SubView::PackageFunctions,
SubView::PackageProcedures,
],
TabKind::DbType { .. } => vec![
SubView::TypeAttributes,
SubView::TypeMethods,
SubView::TypeDeclaration,
SubView::TypeBody,
],
TabKind::Trigger { .. } => vec![SubView::TriggerColumns, SubView::TriggerDeclaration],
TabKind::Script { .. } | TabKind::Function { .. } | TabKind::Procedure { .. } => {
vec![]
}
}
}
pub fn next_sub_view(&mut self) {
let views = self.available_sub_views();
if views.len() <= 1 {
return;
}
if let Some(current) = &self.active_sub_view
&& let Some(idx) = views.iter().position(|v| v == current)
{
self.active_sub_view = Some(views[(idx + 1) % views.len()].clone());
}
}
pub fn prev_sub_view(&mut self) {
let views = self.available_sub_views();
if views.len() <= 1 {
return;
}
if let Some(current) = &self.active_sub_view
&& let Some(idx) = views.iter().position(|v| v == current)
{
let prev = if idx == 0 { views.len() - 1 } else { idx - 1 };
self.active_sub_view = Some(views[prev].clone());
}
}
pub fn sync_grid_for_subview(&mut self) {
if self.table_data_result.is_none()
&& self.query_result.is_some()
&& !matches!(self.active_sub_view, Some(SubView::TableData))
{
self.table_data_result = self.query_result.clone();
}
let reset_grid = |s: &mut Self| {
s.grid_selected_row = 0;
s.grid_selected_col = 0;
s.grid_scroll_row = 0;
s.grid_scroll_col = 0;
s.grid_on_header = true;
s.grid_visual_mode = false;
s.grid_selection_anchor = None;
};
match &self.active_sub_view {
Some(SubView::TableData) => {
if let Some(data) = self.table_data_result.take() {
self.query_result = Some(data);
}
reset_grid(self);
}
Some(SubView::TypeAttributes) => {
self.query_result = self.type_attributes.clone();
reset_grid(self);
}
Some(SubView::TypeMethods) => {
self.query_result = self.type_methods.clone();
reset_grid(self);
}
Some(SubView::TriggerColumns) => {
self.query_result = self.trigger_columns.clone();
reset_grid(self);
}
Some(SubView::TableProperties) => {
self.query_result = Some(QueryResult {
columns: vec![
"Column".to_string(),
"Type".to_string(),
"Nullable".to_string(),
"PK".to_string(),
],
rows: self
.columns
.iter()
.map(|col| {
vec![
col.name.clone(),
col.data_type.clone(),
if col.nullable {
"YES".to_string()
} else {
"NO".to_string()
},
if col.is_primary_key {
"\u{2713}".to_string()
} else {
String::new()
},
]
})
.collect(),
elapsed: None,
});
reset_grid(self);
}
_ => {}
}
}
pub fn content_hash(content: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
pub fn mark_saved(&mut self) {
if let Some(editor) = self.active_editor() {
self.saved_content_hash = Self::content_hash(&editor.content());
}
}
pub fn check_modified(&mut self) {
if let Some(editor) = self.active_editor() {
let current = Self::content_hash(&editor.content());
if current == self.saved_content_hash {
if let Some(e) = self.active_editor_mut() {
e.modified = false;
}
}
}
}
pub fn active_editor(&self) -> Option<&VimEditor> {
match &self.active_sub_view {
Some(SubView::TableDDL) => self.ddl_editor.as_ref(),
Some(SubView::PackageBody) | Some(SubView::TypeBody) => self.body_editor.as_ref(),
Some(SubView::PackageDeclaration)
| Some(SubView::TypeDeclaration)
| Some(SubView::TriggerDeclaration) => self.decl_editor.as_ref(),
None => self.editor.as_ref(), _ => None,
}
}
pub fn active_editor_mut(&mut self) -> Option<&mut VimEditor> {
match &self.active_sub_view {
Some(SubView::TableDDL) => self.ddl_editor.as_mut(),
Some(SubView::PackageBody) | Some(SubView::TypeBody) => self.body_editor.as_mut(),
Some(SubView::PackageDeclaration)
| Some(SubView::TypeDeclaration)
| Some(SubView::TriggerDeclaration) => self.decl_editor.as_mut(),
None => self.editor.as_mut(), _ => None,
}
}
}