use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::collections::HashMap;
use std::time::Duration;
use crate::core::models::DatabaseType;
use crate::ui::state::{
AppState, Focus, LeafKind, Mode, Overlay, ScriptNode, ScriptsMode, TreeNode,
};
use crate::ui::tabs::{CellEdit, RowChange, SubView, TabId, TabKind, WorkspaceTab};
use vimltui::{EditorAction, GutterSign};
pub enum Action {
Quit,
Render,
None,
LoadSchemas {
conn_name: String,
},
SaveSchemaFilter,
LoadChildren {
schema: String,
kind: String,
},
LoadTableData {
tab_id: TabId,
schema: String,
table: String,
},
LoadPackageContent {
tab_id: TabId,
schema: String,
name: String,
},
ExecuteQuery {
tab_id: TabId,
query: String,
start_line: usize,
},
ExecuteQueryNewTab {
tab_id: TabId,
query: String,
start_line: usize,
},
LoadSourceCode {
tab_id: TabId,
schema: String,
name: String,
obj_type: String,
},
OpenNewScript,
OpenScript {
name: String,
},
CloseTab,
SaveScript,
SaveScriptAs {
name: String,
},
ConfirmCloseYes,
ConfirmCloseNo,
Connect,
ConnectByName {
name: String,
},
DisconnectByName {
name: String,
},
SaveConnection,
DeleteConnection {
name: String,
},
CloseResultTab,
OpenThemePicker,
SetTheme {
name: String,
},
ValidateAndSave {
tab_id: TabId,
},
CompileToDb {
tab_id: TabId,
},
OpenScriptConnPicker,
SetScriptConnection {
conn_name: String,
},
CacheColumns {
schema: String,
table: String,
},
ScriptOp {
op: ScriptOperation,
},
ReloadTableData,
SaveGridChanges,
LoadTableDDL {
tab_id: TabId,
schema: String,
table: String,
},
LoadTypeInfo {
tab_id: TabId,
schema: String,
name: String,
},
LoadTriggerInfo {
tab_id: TabId,
schema: String,
name: String,
},
DropObject {
conn_name: String,
schema: String,
name: String,
obj_type: String,
},
RenameObject {
conn_name: String,
schema: String,
old_name: String,
new_name: String,
obj_type: String,
},
CreateFromTemplate {
conn_name: String,
schema: String,
obj_type: String,
},
DuplicateConnection {
source_name: String,
target_group: String,
},
}
pub enum ScriptOperation {
Create {
name: String,
in_collection: Option<String>,
},
Delete {
path: String,
},
DeleteCollection {
name: String,
},
Rename {
old_path: String,
new_name: String,
},
RenameCollection {
old_name: String,
new_name: String,
},
Move {
from: String,
to_collection: Option<String>,
},
}
pub enum InputEvent {
Key(KeyEvent),
Paste(String),
}
pub fn poll_event(timeout: Duration) -> Option<InputEvent> {
if event::poll(timeout).ok()? {
match event::read().ok()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
return Some(InputEvent::Key(key));
}
Event::Paste(text) => return Some(InputEvent::Paste(text)),
_ => {}
}
}
None
}
pub fn handle_key(state: &mut AppState, key: KeyEvent) -> Action {
let (editor_in_insert, editor_in_special) = if state.focus == Focus::TabContent {
if let Some(e) = state.active_tab().and_then(|t| t.active_editor()) {
let in_insert = matches!(e.mode, vimltui::VimMode::Insert | vimltui::VimMode::Replace)
|| e.command_active
|| e.search.active;
let in_special =
!matches!(e.mode, vimltui::VimMode::Normal) || e.command_active || e.search.active;
(in_insert, in_special)
} else {
(false, false)
}
} else {
(false, false)
};
if state.overlay.is_none()
&& !editor_in_insert
&& !state.tree_state.search_active
&& let Some(action) = handle_global_leader(state, key)
{
return action;
}
if key.code == KeyCode::Char('s')
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& state.overlay.is_none()
&& state.focus == Focus::TabContent
&& let Some(tab) = state.active_tab()
{
match &tab.kind {
TabKind::Script { .. } => return Action::SaveScript,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. } => {
return Action::CompileToDb { tab_id: tab.id };
}
_ => {}
}
}
if let Some(overlay) = &state.overlay {
return match overlay {
Overlay::ConnectionDialog => handle_connection_dialog(state, key),
Overlay::Help => handle_help_overlay(state, key),
Overlay::ObjectFilter => handle_object_filter(state, key),
Overlay::ConnectionMenu => handle_conn_menu(state, key),
Overlay::GroupMenu => handle_group_menu(state, key),
Overlay::ConfirmClose => handle_confirm_close(state, key),
Overlay::ConfirmQuit => handle_confirm_quit(state, key),
Overlay::SaveScriptName => handle_save_script_name(state, key),
Overlay::ScriptConnection => handle_script_conn_picker(state, key),
Overlay::ThemePicker => handle_theme_picker(state, key),
Overlay::BindVariables => handle_bind_variables(state, key),
Overlay::SaveGridChanges => handle_save_grid_confirm(state, key),
Overlay::RenameObject => match key.code {
KeyCode::Enter => {
let new_name = state.sidebar_rename_buf.trim().to_string();
state.overlay = None;
if let Some(action) = state.sidebar_pending_action.take() {
if new_name.is_empty() || new_name == action.name {
state.sidebar_rename_buf.clear();
state.status_message = "Rename cancelled".to_string();
Action::Render
} else {
state.sidebar_rename_buf.clear();
Action::RenameObject {
conn_name: action.conn_name,
schema: action.schema,
old_name: action.name,
new_name,
obj_type: action.obj_type,
}
}
} else {
Action::Render
}
}
KeyCode::Esc => {
state.overlay = None;
state.sidebar_pending_action = None;
state.sidebar_rename_buf.clear();
Action::Render
}
KeyCode::Char(c) => {
state.sidebar_rename_buf.push(c);
Action::Render
}
KeyCode::Backspace => {
state.sidebar_rename_buf.pop();
Action::Render
}
_ => Action::Render,
},
Overlay::ConfirmDropObject => match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
state.overlay = None;
if let Some(action) = state.sidebar_pending_action.take() {
Action::DropObject {
conn_name: action.conn_name,
schema: action.schema,
name: action.name,
obj_type: action.obj_type,
}
} else {
Action::Render
}
}
_ => {
state.overlay = None;
state.sidebar_pending_action = None;
state.status_message = "Drop cancelled".to_string();
Action::Render
}
},
Overlay::ConfirmCompile => match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
state.overlay = None;
state.compile_confirmed = true;
if let Some(tab) = state.active_tab() {
let tab_id = tab.id;
Action::CompileToDb { tab_id }
} else {
Action::Render
}
}
_ => {
state.overlay = None;
state.status_message = "Compile cancelled".to_string();
Action::Render
}
},
};
}
if state.tree_state.search_active {
return handle_sidebar_search(state, key);
}
if let Some(action) = handle_global_normal_keys(state, key, editor_in_special) {
return action;
}
if let Some(action) = handle_spatial_navigation(state, key, editor_in_special) {
return action;
}
match state.focus {
Focus::Sidebar => handle_sidebar(state, key),
Focus::ScriptsPanel => handle_scripts_panel(state, key),
Focus::TabContent => handle_tab_content(state, key),
}
}
fn handle_global_normal_keys(
state: &mut AppState,
key: KeyEvent,
in_editor_special_mode: bool,
) -> Option<Action> {
if state.mode != Mode::Normal || in_editor_special_mode {
return None;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('1') => {
state.focus = Focus::Sidebar;
return Some(Action::Render);
}
KeyCode::Char('2') => {
state.focus = Focus::ScriptsPanel;
return Some(Action::Render);
}
KeyCode::Char('3') => {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut() {
tab.grid_focused = false;
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
}
return Some(Action::Render);
}
KeyCode::Char('4') => {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut()
&& !tab.result_tabs.is_empty()
{
tab.grid_focused = true;
tab.sub_focus = crate::ui::tabs::SubFocus::Results;
}
return Some(Action::Render);
}
_ => {}
}
}
if key.modifiers == KeyModifiers::NONE && state.focus != Focus::TabContent {
match key.code {
KeyCode::Char('1') => {
state.focus = Focus::Sidebar;
return Some(Action::Render);
}
KeyCode::Char('2') => {
state.focus = Focus::ScriptsPanel;
return Some(Action::Render);
}
KeyCode::Char('3') => {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut() {
tab.grid_focused = false;
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
}
return Some(Action::Render);
}
KeyCode::Char('4') => {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut()
&& !tab.result_tabs.is_empty()
{
tab.grid_focused = true;
tab.sub_focus = crate::ui::tabs::SubFocus::Results;
}
return Some(Action::Render);
}
_ => {}
}
}
match key.code {
KeyCode::Char('q') => {
let has_unsaved = state.tabs.iter().any(|t| {
t.editor.as_ref().is_some_and(|e| e.modified)
|| t.body_editor.as_ref().is_some_and(|e| e.modified)
|| t.decl_editor.as_ref().is_some_and(|e| e.modified)
|| !t.grid_changes.is_empty()
});
if has_unsaved {
if let Some(idx) = state.tabs.iter().position(|t| {
t.editor.as_ref().is_some_and(|e| e.modified)
|| t.body_editor.as_ref().is_some_and(|e| e.modified)
|| t.decl_editor.as_ref().is_some_and(|e| e.modified)
|| !t.grid_changes.is_empty()
}) {
state.active_tab_idx = idx;
state.focus = Focus::TabContent;
}
state.overlay = Some(Overlay::ConfirmQuit);
return Some(Action::Render);
}
Some(Action::Quit)
}
KeyCode::Char('?') => {
state.overlay = Some(Overlay::Help);
Some(Action::Render)
}
KeyCode::Char('a') if state.focus == Focus::Sidebar => {
let groups = state.available_groups();
let current_group = state
.selected_tree_index()
.and_then(|idx| {
let mut i = idx;
loop {
if let TreeNode::Group { name, .. } = &state.tree[i] {
return Some(name.clone());
}
if i == 0 {
break;
}
i -= 1;
}
None
})
.unwrap_or_else(|| "Default".to_string());
state.connection_form = crate::ui::state::ConnectionFormState::new();
state.connection_form.group = current_group;
state.connection_form.group_options = groups;
state.overlay = Some(Overlay::ConnectionDialog);
Some(Action::Render)
}
KeyCode::Char('n') if state.focus == Focus::ScriptsPanel => Some(Action::OpenNewScript),
KeyCode::Char('F') => Some(handle_filter_key(state)),
KeyCode::Char(']') => {
if !state.tabs.is_empty() {
state.active_tab_idx = (state.active_tab_idx + 1) % state.tabs.len();
state.focus = Focus::TabContent;
}
Some(Action::Render)
}
KeyCode::Char('[') => {
if !state.tabs.is_empty() {
state.active_tab_idx = if state.active_tab_idx == 0 {
state.tabs.len() - 1
} else {
state.active_tab_idx - 1
};
state.focus = Focus::TabContent;
}
Some(Action::Render)
}
KeyCode::Char('}') => {
if let Some(tab) = state.active_tab()
&& tab.grid_focused
&& tab.result_tabs.len() > 1
{
let tab = state.active_tab_mut().expect("checked");
sync_grid_to_result_tab(tab);
tab.active_result_idx = (tab.active_result_idx + 1) % tab.result_tabs.len();
return Some(Action::Render);
}
if let Some(tab) = state.active_tab_mut() {
tab.next_sub_view();
tab.sync_grid_for_subview();
}
maybe_load_ddl(state)
}
KeyCode::Char('{') => {
if let Some(tab) = state.active_tab()
&& tab.grid_focused
&& tab.result_tabs.len() > 1
{
let tab = state.active_tab_mut().expect("checked");
sync_grid_to_result_tab(tab);
tab.active_result_idx = if tab.active_result_idx == 0 {
tab.result_tabs.len() - 1
} else {
tab.active_result_idx - 1
};
return Some(Action::Render);
}
if let Some(tab) = state.active_tab_mut() {
tab.prev_sub_view();
tab.sync_grid_for_subview();
}
maybe_load_ddl(state)
}
_ => None,
}
}
fn maybe_load_ddl(state: &AppState) -> Option<Action> {
let tab = state.active_tab()?;
if tab.active_sub_view.as_ref() != Some(&SubView::TableDDL) {
return Some(Action::Render);
}
if let Some(editor) = &tab.ddl_editor
&& !editor.content().is_empty()
{
return Some(Action::Render);
}
if let TabKind::Table { schema, table, .. } = &tab.kind {
Some(Action::LoadTableDDL {
tab_id: tab.id,
schema: schema.clone(),
table: table.clone(),
})
} else {
Some(Action::Render)
}
}
fn handle_spatial_navigation(
state: &mut AppState,
key: KeyEvent,
in_editor_special_mode: bool,
) -> Option<Action> {
if !key.modifiers.contains(KeyModifiers::CONTROL) || in_editor_special_mode {
return None;
}
use crate::ui::tabs::SubFocus;
let sub = state
.active_tab()
.map(|t| t.sub_focus)
.unwrap_or(SubFocus::Editor);
let has_tabs = !state.tabs.is_empty();
match key.code {
KeyCode::Char('h') | KeyCode::Left => {
match (state.focus, sub) {
(Focus::TabContent, SubFocus::Editor) => state.focus = Focus::Sidebar,
(Focus::TabContent, SubFocus::Results) => state.focus = Focus::ScriptsPanel,
(Focus::TabContent, SubFocus::QueryView) => {
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Results;
}
}
_ => {}
}
Some(Action::Render)
}
KeyCode::Char('l') | KeyCode::Right => {
match (state.focus, sub) {
(Focus::Sidebar, _) if has_tabs => {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Editor;
tab.grid_focused = false;
}
}
(Focus::ScriptsPanel, _) if has_tabs => {
let has_bottom = state
.active_tab()
.is_some_and(|t| !t.result_tabs.is_empty() || t.query_result.is_some());
if has_bottom {
state.focus = Focus::TabContent;
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Results;
tab.grid_focused = true;
}
} else {
state.focus = Focus::TabContent;
}
}
(Focus::TabContent, SubFocus::Results) => {
let has_query = state.active_tab().is_some_and(|t| {
let idx = t.active_result_idx;
(idx < t.result_tabs.len() && t.result_tabs[idx].query_editor.is_some())
|| t.grid_query_editor.is_some()
});
if has_query && let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::QueryView;
}
}
_ => {}
}
Some(Action::Render)
}
KeyCode::Char('j') | KeyCode::Down => {
match (state.focus, sub) {
(Focus::Sidebar, _) => state.focus = Focus::ScriptsPanel,
(Focus::TabContent, SubFocus::Editor) => {
let has_error_pane = state
.active_tab()
.is_some_and(|t| t.grid_error_editor.is_some());
let has_bottom = state
.active_tab()
.is_some_and(|t| !t.result_tabs.is_empty() || t.query_result.is_some());
if has_error_pane {
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Results;
tab.grid_focused = false;
}
} else if has_bottom && let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Results;
tab.grid_focused = true;
}
}
_ => {}
}
Some(Action::Render)
}
KeyCode::Char('k') | KeyCode::Up => {
match (state.focus, sub) {
(Focus::ScriptsPanel, _) => state.focus = Focus::Sidebar,
(Focus::TabContent, SubFocus::Results) => {
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Editor;
tab.grid_focused = false;
}
}
(Focus::TabContent, SubFocus::QueryView) => {
if let Some(tab) = state.active_tab_mut() {
tab.sub_focus = SubFocus::Editor;
tab.grid_focused = false;
}
}
_ => {}
}
Some(Action::Render)
}
_ => None,
}
}
fn should_exit_sub_pane(tab: &WorkspaceTab, sub_focus: crate::ui::tabs::SubFocus) -> bool {
use crate::ui::tabs::SubFocus;
let idx = tab.active_result_idx;
match sub_focus {
SubFocus::Results => {
if idx < tab.result_tabs.len() {
if let Some(editor) = &tab.result_tabs[idx].error_editor {
matches!(editor.mode, vimltui::VimMode::Normal) && !editor.search.active
} else {
!tab.grid_visual_mode
}
} else {
!tab.grid_visual_mode
}
}
SubFocus::QueryView => {
if idx < tab.result_tabs.len() {
if let Some(editor) = &tab.result_tabs[idx].query_editor {
matches!(editor.mode, vimltui::VimMode::Normal) && !editor.search.active
} else {
true
}
} else {
true
}
}
_ => true,
}
}
fn handle_tab_content(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let sub_view = state.tabs[tab_idx].active_sub_view.clone();
match sub_view {
Some(SubView::TableData) => {
let tab = &state.tabs[tab_idx];
let has_error = tab.grid_error_editor.is_some();
let sub = tab.sub_focus;
if has_error {
use crate::ui::tabs::SubFocus;
match sub {
SubFocus::Results => {
return handle_table_error_editor(state, key, false);
}
SubFocus::QueryView => {
return handle_table_error_editor(state, key, true);
}
SubFocus::Editor => {}
}
}
handle_tab_data_grid(state, key)
}
Some(SubView::TableProperties) => {
Action::None
}
Some(SubView::TableDDL) => handle_tab_editor(state, key),
Some(SubView::PackageBody) | Some(SubView::PackageDeclaration) => {
let tab = &state.tabs[tab_idx];
let has_error = tab.grid_error_editor.is_some();
let sub = tab.sub_focus;
if has_error {
use crate::ui::tabs::SubFocus;
match sub {
SubFocus::Results => {
return handle_table_error_editor(state, key, false);
}
SubFocus::QueryView => {
return handle_table_error_editor(state, key, true);
}
SubFocus::Editor => {}
}
}
handle_tab_editor(state, key)
}
Some(SubView::PackageFunctions) | Some(SubView::PackageProcedures) => {
handle_tab_package_list(state, key)
}
Some(SubView::TypeAttributes)
| Some(SubView::TypeMethods)
| Some(SubView::TriggerColumns) => handle_tab_data_grid(state, key),
Some(SubView::TypeDeclaration)
| Some(SubView::TypeBody)
| Some(SubView::TriggerDeclaration) => handle_tab_editor(state, key),
None => {
use crate::ui::tabs::SubFocus;
let tab = &state.tabs[state.active_tab_idx];
let has_bottom = tab.query_result.is_some() || !tab.result_tabs.is_empty();
let sub_focus = tab.sub_focus;
if (sub_focus == SubFocus::Results || sub_focus == SubFocus::QueryView)
&& key.code == KeyCode::Esc
&& should_exit_sub_pane(&state.tabs[state.active_tab_idx], sub_focus)
{
let tab = &mut state.tabs[state.active_tab_idx];
tab.sub_focus = SubFocus::Editor;
tab.grid_focused = false;
return Action::Render;
}
match sub_focus {
SubFocus::Editor if !has_bottom => handle_tab_editor(state, key),
SubFocus::Editor => handle_tab_editor(state, key),
SubFocus::Results => {
let has_error = {
let tab = &state.tabs[state.active_tab_idx];
let idx = tab.active_result_idx;
idx < tab.result_tabs.len() && tab.result_tabs[idx].error_editor.is_some()
};
if has_error {
let tab = &mut state.tabs[state.active_tab_idx];
let idx = tab.active_result_idx;
if let Some(editor) = tab.result_tabs[idx].error_editor.as_mut() {
editor.handle_key(key);
}
return Action::Render;
}
handle_tab_data_grid(state, key)
}
SubFocus::QueryView => {
let tab = &mut state.tabs[state.active_tab_idx];
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len()
&& let Some(editor) = tab.result_tabs[idx].query_editor.as_mut()
{
editor.handle_key(key);
}
Action::Render
}
}
}
}
}
fn handle_tab_editor(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
let tab_id = tab.id;
let is_script = matches!(tab.kind, TabKind::Script { .. });
let is_source_tab = matches!(
tab.kind,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. }
);
let in_insert = tab
.active_editor()
.is_some_and(|e| matches!(e.mode, vimltui::VimMode::Insert | vimltui::VimMode::Replace));
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if in_insert {
if ctrl {
match key.code {
KeyCode::Char(' ') => {
update_completion_impl(state, true);
return Action::Render;
}
KeyCode::Char('n') => {
if let Some(ref mut cmp) = state.completion {
cmp.next();
}
return Action::Render;
}
KeyCode::Char('p') => {
if let Some(ref mut cmp) = state.completion {
cmp.prev();
}
return Action::Render;
}
KeyCode::Char('y') => {
if let Some(cmp) = state.completion.take() {
accept_completion(state, &cmp);
if let Some(action) = update_completion_impl(state, false) {
return action;
}
}
return Action::Render;
}
_ => {}
}
}
if key.code == KeyCode::Enter && state.completion.is_some() {
if let Some(cmp) = state.completion.take() {
accept_completion(state, &cmp);
if let Some(action) = update_completion_impl(state, false) {
return action;
}
}
return Action::Render;
}
if key.code == KeyCode::Esc && state.completion.is_some() {
state.completion = None;
}
}
let (action, still_insert, needs_diag) = {
let tab = &mut state.tabs[tab_idx];
if let Some(editor) = tab.active_editor_mut() {
let action = match editor.handle_key(key) {
EditorAction::Handled => Action::Render,
EditorAction::Unhandled(_) => Action::None,
EditorAction::Save => {
if is_source_tab {
Action::ValidateAndSave { tab_id }
} else {
Action::SaveScript
}
}
EditorAction::Close => Action::CloseTab,
EditorAction::ForceClose => Action::Quit,
EditorAction::SaveAndClose => {
if is_script {
return Action::SaveScript;
}
Action::CloseTab
}
};
let still_insert = matches!(editor.mode, vimltui::VimMode::Insert);
let needs_diag = !still_insert && in_insert && editor.modified && state.metadata_ready;
(action, still_insert, needs_diag)
} else {
return Action::None;
}
};
{
let tab = &state.tabs[tab_idx];
let original = match &tab.active_sub_view {
Some(SubView::PackageDeclaration) | Some(SubView::TypeDeclaration) => {
tab.original_decl.clone()
}
Some(SubView::PackageBody) | Some(SubView::TypeBody) => tab.original_body.clone(),
None if matches!(
tab.kind,
TabKind::Function { .. } | TabKind::Procedure { .. }
) =>
{
tab.original_source.clone()
}
_ => None,
};
if let Some(orig) = original {
let tab = &mut state.tabs[tab_idx];
if let Some(editor) = tab.active_editor_mut() {
let signs = compute_diff_signs(&orig, &editor.lines);
if signs.is_empty() {
editor.gutter = None;
} else {
let mut config = editor.gutter.take().unwrap_or_default();
config.signs = signs;
editor.gutter = Some(config);
}
}
}
}
if still_insert {
if let Some(cache_action) = update_completion(state) {
return cache_action;
}
} else {
state.completion = None;
}
if needs_diag {
let tab = &state.tabs[tab_idx];
let is_plsql = matches!(
tab.kind,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. }
);
if is_plsql {
state.diagnostics.clear();
} else {
let script_conn = tab.kind.conn_name().map(|s| s.to_string());
let lines = tab
.active_editor()
.map(|e| e.lines.clone())
.unwrap_or_default();
state.diagnostics =
crate::ui::diagnostics::check_sql(state, &lines, script_conn.as_deref());
}
}
action
}
fn update_completion(state: &mut AppState) -> Option<Action> {
update_completion_impl(state, false)
}
fn update_completion_impl(state: &mut AppState, force: bool) -> Option<Action> {
use crate::ui::completion::{
CompletionState, build_completions, build_completions_forced, is_after_dot,
word_prefix_at_cursor,
};
let tab = match state.tabs.get(state.active_tab_idx) {
Some(t) => t,
None => {
state.completion = None;
return None;
}
};
let editor = match tab.active_editor() {
Some(e) => e,
None => {
state.completion = None;
return None;
}
};
let row = editor.cursor_row;
let col = editor.cursor_col;
let line = editor.current_line();
let (prefix, start_col) = word_prefix_at_cursor(line, col);
let dot_mode = prefix.is_empty() && is_after_dot(line, col);
if prefix.is_empty() && !dot_mode && !force {
if let Some(cmp) = state.completion.take() {
let old_prefix_upper = cmp.prefix.to_uppercase();
if !old_prefix_upper.is_empty() {
let exact: Vec<_> = cmp
.items
.iter()
.filter(|item| item.label.to_uppercase() == old_prefix_upper)
.collect();
if exact.len() == 1 && exact[0].label != cmp.prefix {
let tab = state.tabs.get_mut(state.active_tab_idx);
if let Some(editor) = tab.and_then(|t| t.active_editor_mut()) {
let r = cmp.origin_row;
if r < editor.lines.len() {
let line = &editor.lines[r];
let start = cmp.origin_col.min(line.len());
let end = (start + cmp.prefix.len()).min(line.len());
let mut new_line = String::with_capacity(line.len());
new_line.push_str(&line[..start]);
new_line.push_str(&exact[0].label);
new_line.push_str(&line[end..]);
editor.lines[r] = new_line;
let diff = exact[0].label.len() as isize - cmp.prefix.len() as isize;
if editor.cursor_row == r && editor.cursor_col > start {
editor.cursor_col =
(editor.cursor_col as isize + diff).max(0) as usize;
}
}
}
}
}
}
return None;
}
let editor_row = row; let total_lines = editor.lines.len();
let mut block_start = row;
while block_start > 0 && !editor.lines[block_start - 1].trim().is_empty() {
block_start -= 1;
}
let mut block_end = row + 1;
while block_end < total_lines && !editor.lines[block_end].trim().is_empty() {
block_end += 1;
}
let lines: Vec<String> = editor.lines[block_start..block_end].to_vec();
let row = row - block_start;
let items = if force {
build_completions_forced(state, &lines, row, col)
} else {
build_completions(state, &lines, row, col)
};
let has_dot = dot_mode || {
let before = &lines[row][..col.min(lines[row].len())];
let bytes = before.as_bytes();
let mut p = col.min(bytes.len());
while p > 0 && (bytes[p - 1].is_ascii_alphanumeric() || bytes[p - 1] == b'_') {
p -= 1;
}
p > 0 && bytes[p - 1] == b'.'
};
let cache_action = if items.is_empty() && has_dot {
let before = &lines[row][..col.min(lines[row].len())];
let bytes = before.as_bytes();
let mut p = col.min(bytes.len());
while p > 0 && (bytes[p - 1].is_ascii_alphanumeric() || bytes[p - 1] == b'_') {
p -= 1;
}
if p > 0 && bytes[p - 1] == b'.' {
p -= 1;
}
let te = p;
let mut ts = te;
while ts > 0 && (bytes[ts - 1].is_ascii_alphanumeric() || bytes[ts - 1] == b'_') {
ts -= 1;
}
let table_ref = &before[ts..te];
if !table_ref.is_empty() && !crate::ui::completion::is_known_schema(state, table_ref) {
let resolved = resolve_table_for_cache(state, &lines, row, table_ref);
if let Some((schema, table)) = resolved {
let key = format!("{}.{}", schema.to_uppercase(), table.to_uppercase());
if !state.column_cache.contains_key(&key) {
Some(Action::CacheColumns { schema, table })
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
};
if items.is_empty() {
state.completion = None;
return cache_action;
}
let prev_cursor = state
.completion
.as_ref()
.map(|c| c.cursor.min(items.len().saturating_sub(1)))
.unwrap_or(0);
state.completion = Some(CompletionState {
items,
cursor: prev_cursor,
prefix: prefix.to_string(),
origin_row: editor_row,
origin_col: start_col,
});
cache_action
}
fn resolve_table_for_cache(
state: &AppState,
lines: &[String],
_row: usize,
table_ref: &str,
) -> Option<(String, String)> {
use crate::ui::completion::find_schema_for_table;
let block: Vec<String> = lines.to_vec();
let resolved = crate::ui::completion::resolve_table_name(&block, table_ref);
let table_name = resolved.as_deref().unwrap_or(table_ref);
let schema = find_schema_for_table(state, table_name)?;
Some((schema, table_name.to_string()))
}
fn accept_completion(state: &mut AppState, cmp: &crate::ui::completion::CompletionState) {
use crate::ui::completion::CompletionKind;
let item = match cmp.selected() {
Some(s) => s.clone(),
None => return,
};
let needs_parens = match item.kind {
CompletionKind::Function | CompletionKind::Procedure => true,
CompletionKind::Keyword => matches!(item.label.as_str(), "IN" | "EXISTS" | "NOT IN"),
_ => false,
};
let (insert_text, cursor_inside_parens) = match item.kind {
CompletionKind::Alias | CompletionKind::Schema => (format!("{}.", item.label), false),
_ if needs_parens => (format!("{}()", item.label), true),
_ => (item.label, false),
};
let tab = match state.tabs.get_mut(state.active_tab_idx) {
Some(t) => t,
None => return,
};
let editor = match tab.active_editor_mut() {
Some(e) => e,
None => return,
};
let row = cmp.origin_row;
if row >= editor.lines.len() {
return;
}
let line = &editor.lines[row];
let start = cmp.origin_col.min(line.len());
let end = editor.cursor_col.min(line.len());
let mut new_line = String::with_capacity(line.len() + insert_text.len());
new_line.push_str(&line[..start]);
new_line.push_str(&insert_text);
if end < line.len() {
new_line.push_str(&line[end..]);
}
editor.lines[row] = new_line;
editor.cursor_col = if cursor_inside_parens {
start + insert_text.len() - 1
} else {
start + insert_text.len()
};
editor.modified = true;
}
fn handle_tab_data_grid(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let is_script = matches!(state.tabs[tab_idx].kind, TabKind::Script { .. });
if is_script {
let tab = &mut state.tabs[tab_idx];
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
let rt = &tab.result_tabs[idx];
tab.query_result = Some(rt.result.clone());
tab.grid_scroll_row = rt.scroll_row;
tab.grid_selected_row = rt.selected_row;
tab.grid_selected_col = rt.selected_col;
tab.grid_selection_anchor = rt.selection_anchor;
}
match key.code {
KeyCode::Char('}') => {
if tab.result_tabs.len() > 1 {
sync_grid_to_result_tab(tab);
tab.active_result_idx = (tab.active_result_idx + 1) % tab.result_tabs.len();
}
return Action::Render;
}
KeyCode::Char('{') => {
if tab.result_tabs.len() > 1 {
sync_grid_to_result_tab(tab);
tab.active_result_idx = if tab.active_result_idx == 0 {
tab.result_tabs.len() - 1
} else {
tab.active_result_idx - 1
};
}
return Action::Render;
}
_ => {}
}
}
let tab = &mut state.tabs[tab_idx];
if tab.grid_editing.is_some() && state.mode == Mode::Insert {
return handle_grid_cell_edit(state, key);
}
if tab.grid_error_editor.is_some() && key.code == KeyCode::Esc {
tab.grid_error_editor = None;
tab.grid_query_editor = None;
return Action::Render;
}
let is_table_tab = matches!(tab.kind, TabKind::Table { .. });
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let vh = tab.grid_visible_height.max(1);
let visual = tab.grid_visual_mode;
let action = match key.code {
KeyCode::Char('i') if is_table_tab && !visual => {
if row_count > 0 && col_count > 0 {
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
}
return Action::Render;
}
KeyCode::Char('o') if is_table_tab && !visual => {
if let Some(ref mut qr) = tab.query_result {
let new_row: Vec<String> = qr.columns.iter().map(|_| "NULL".to_string()).collect();
let insert_pos = (tab.grid_selected_row + 1).min(qr.rows.len());
qr.rows.insert(insert_pos, new_row.clone());
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k >= insert_pos {
shifted.insert(k + 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
tab.grid_changes
.insert(insert_pos, RowChange::New { values: new_row });
tab.grid_selected_row = insert_pos;
tab.grid_selected_col = 0;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh - 1);
}
}
return Action::Render;
}
KeyCode::Char('d')
if is_table_tab && !visual && !key.modifiers.contains(KeyModifiers::CONTROL) =>
{
if state.pending_d {
state.pending_d = false;
if row_count > 0 {
let row = tab.grid_selected_row;
if matches!(tab.grid_changes.get(&row), Some(RowChange::New { .. })) {
tab.grid_changes.remove(&row);
if let Some(ref mut qr) = tab.query_result
&& row < qr.rows.len()
{
qr.rows.remove(row);
}
let mut shifted: std::collections::HashMap<usize, _> =
std::collections::HashMap::new();
for (k, v) in tab.grid_changes.drain() {
if k > row {
shifted.insert(k - 1, v);
} else {
shifted.insert(k, v);
}
}
tab.grid_changes = shifted;
let new_count =
tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if tab.grid_selected_row >= new_count && new_count > 0 {
tab.grid_selected_row = new_count - 1;
}
} else {
tab.grid_changes.insert(row, RowChange::Deleted);
}
}
return Action::Render;
} else {
state.pending_d = true;
return Action::Render;
}
}
KeyCode::Char('u')
if is_table_tab && !visual && !key.modifiers.contains(KeyModifiers::CONTROL) =>
{
if !tab.grid_changes.is_empty() {
tab.grid_changes.clear();
state.status_message = "Changes discarded".to_string();
return Action::ReloadTableData;
}
return Action::Render;
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) && is_table_tab => {
if !tab.grid_changes.is_empty() {
let modified = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Modified { .. }))
.count();
let new = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::New { .. }))
.count();
let deleted = tab
.grid_changes
.values()
.filter(|c| matches!(c, RowChange::Deleted))
.count();
state.status_message =
format!("Save: {modified} modified, {new} new, {deleted} deleted — y/n?");
state.overlay = Some(Overlay::SaveGridChanges);
} else {
state.status_message = "No pending changes".to_string();
}
return Action::Render;
}
KeyCode::Char('v') => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
} else {
tab.grid_visual_mode = true;
tab.grid_selection_anchor = Some((tab.grid_selected_row, tab.grid_selected_col));
}
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
}
Action::Render
}
KeyCode::Char('h') | KeyCode::Left => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
}
Action::Render
}
KeyCode::Char('l') | KeyCode::Right => {
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
}
Action::Render
}
KeyCode::Char('e') => {
if col_count > 0 {
if tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
if tab.grid_selected_row >= tab.grid_scroll_row + vh {
tab.grid_scroll_row = tab.grid_selected_row - vh + 1;
}
}
}
Action::Render
}
KeyCode::Char('b') => {
if tab.grid_selected_col > 0 {
tab.grid_selected_col -= 1;
} else if tab.grid_selected_row > 0 {
tab.grid_selected_row -= 1;
tab.grid_selected_col = col_count.saturating_sub(1);
if tab.grid_selected_row < tab.grid_scroll_row {
tab.grid_scroll_row = tab.grid_selected_row;
}
}
Action::Render
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = vh / 2;
tab.grid_selected_row = (tab.grid_selected_row + half).min(row_count.saturating_sub(1));
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = vh / 2;
tab.grid_selected_row = tab.grid_selected_row.saturating_sub(half);
tab.grid_scroll_row = tab.grid_selected_row.saturating_sub(vh / 2);
Action::Render
}
KeyCode::Char('g') => {
tab.grid_selected_row = 0;
tab.grid_selected_col = 0;
tab.grid_scroll_row = 0;
Action::Render
}
KeyCode::Char('G') => {
if row_count > 0 {
tab.grid_selected_row = row_count - 1;
tab.grid_scroll_row = row_count.saturating_sub(vh);
}
Action::Render
}
KeyCode::Char('y') => {
grid_yank(tab);
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
Action::Render
}
KeyCode::Esc => {
if visual {
tab.grid_visual_mode = false;
tab.grid_selection_anchor = None;
} else {
tab.grid_focused = false;
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
}
Action::Render
}
_ => Action::None,
};
if matches!(key.code, KeyCode::Char('y')) {
state.status_message = "Copied to clipboard".to_string();
}
if is_script {
let tab = &mut state.tabs[tab_idx];
sync_grid_to_result_tab(tab);
}
action
}
fn handle_grid_cell_edit(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
match key.code {
KeyCode::Esc | KeyCode::Enter => {
if let Some((row, col)) = tab.grid_editing.take() {
let new_val = if tab.grid_edit_buffer.is_empty() {
"NULL".to_string()
} else {
tab.grid_edit_buffer.clone()
};
let original = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
if let Some(ref mut qr) = tab.query_result
&& let Some(r) = qr.rows.get_mut(row)
&& let Some(cell) = r.get_mut(col)
{
*cell = new_val.clone();
}
if new_val != original {
match tab.grid_changes.get_mut(&row) {
Some(RowChange::Modified { edits }) => {
if let Some(e) = edits.iter_mut().find(|e| e.col == col) {
e.value = new_val;
} else {
edits.push(CellEdit {
col,
original,
value: new_val,
});
}
}
Some(RowChange::New { values }) => {
if let Some(v) = values.get_mut(col) {
*v = new_val;
}
}
Some(RowChange::Deleted) => {}
None => {
tab.grid_changes.insert(
row,
RowChange::Modified {
edits: vec![CellEdit {
col,
original,
value: new_val,
}],
},
);
}
}
}
}
tab.grid_edit_buffer.clear();
tab.grid_edit_cursor = 0;
state.mode = Mode::Normal;
Action::Render
}
KeyCode::Tab => {
let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let _ = handle_grid_cell_edit(state, esc_event);
let tab = &mut state.tabs[tab_idx];
let col_count = tab
.query_result
.as_ref()
.map(|r| r.columns.len())
.unwrap_or(0);
let row_count = tab.query_result.as_ref().map(|r| r.rows.len()).unwrap_or(0);
if col_count > 0 && tab.grid_selected_col + 1 < col_count {
tab.grid_selected_col += 1;
} else if tab.grid_selected_row + 1 < row_count {
tab.grid_selected_col = 0;
tab.grid_selected_row += 1;
}
let row = tab.grid_selected_row;
let col = tab.grid_selected_col;
let val = tab
.query_result
.as_ref()
.and_then(|r| r.rows.get(row))
.and_then(|r| r.get(col))
.cloned()
.unwrap_or_default();
let val = if val == "NULL" { String::new() } else { val };
let cursor = val.len();
tab.grid_editing = Some((row, col));
tab.grid_edit_buffer = val;
tab.grid_edit_cursor = cursor;
state.mode = Mode::Insert;
Action::Render
}
KeyCode::Backspace => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Delete => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_buffer.remove(tab.grid_edit_cursor);
}
Action::Render
}
KeyCode::Left => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor > 0 {
tab.grid_edit_cursor -= 1;
}
Action::Render
}
KeyCode::Right => {
let tab = &mut state.tabs[tab_idx];
if tab.grid_edit_cursor < tab.grid_edit_buffer.len() {
tab.grid_edit_cursor += 1;
}
Action::Render
}
KeyCode::Home => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = 0;
Action::Render
}
KeyCode::End => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_cursor = tab.grid_edit_buffer.len();
Action::Render
}
KeyCode::Char(c) => {
let tab = &mut state.tabs[tab_idx];
tab.grid_edit_buffer.insert(tab.grid_edit_cursor, c);
tab.grid_edit_cursor += 1;
Action::Render
}
_ => Action::None,
}
}
fn handle_table_error_editor(state: &mut AppState, key: KeyEvent, is_query: bool) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
if key.code == KeyCode::Esc {
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_ref()
} else {
tab.grid_error_editor.as_ref()
};
let in_normal =
editor.is_some_and(|e| matches!(e.mode, vimltui::VimMode::Normal) && !e.search.active);
if in_normal {
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('l') | KeyCode::Right => {
if !is_query {
let tab = &mut state.tabs[tab_idx];
if tab.grid_query_editor.is_some() {
tab.sub_focus = crate::ui::tabs::SubFocus::QueryView;
}
}
return Action::Render;
}
KeyCode::Char('h') | KeyCode::Left => {
if is_query {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Results;
}
return Action::Render;
}
KeyCode::Char('k') | KeyCode::Up => {
let tab = &mut state.tabs[tab_idx];
tab.sub_focus = crate::ui::tabs::SubFocus::Editor;
return Action::Render;
}
_ => {}
}
}
let tab = &mut state.tabs[tab_idx];
let editor = if is_query {
tab.grid_query_editor.as_mut()
} else {
tab.grid_error_editor.as_mut()
};
if let Some(ed) = editor {
let _ = ed.handle_key(key);
}
Action::Render
}
fn handle_save_grid_confirm(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
state.overlay = None;
Action::SaveGridChanges
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.overlay = None;
state.status_message = "Save cancelled".to_string();
Action::Render
}
_ => Action::None,
}
}
fn sync_grid_to_result_tab(tab: &mut WorkspaceTab) {
let idx = tab.active_result_idx;
if idx < tab.result_tabs.len() {
tab.result_tabs[idx].scroll_row = tab.grid_scroll_row;
tab.result_tabs[idx].selected_row = tab.grid_selected_row;
tab.result_tabs[idx].selected_col = tab.grid_selected_col;
tab.result_tabs[idx].selection_anchor = tab.grid_selection_anchor;
}
}
fn grid_yank(tab: &WorkspaceTab) {
let result = match &tab.query_result {
Some(r) => r,
None => return,
};
if result.rows.is_empty() {
return;
}
let (sr, sc, er, ec) = match tab.grid_selection_anchor {
Some((ar, ac)) => {
let r1 = ar.min(tab.grid_selected_row);
let r2 = ar.max(tab.grid_selected_row);
let c1 = ac.min(tab.grid_selected_col);
let c2 = ac.max(tab.grid_selected_col);
(r1, c1, r2, c2)
}
None => {
let col_count = result.columns.len().saturating_sub(1);
(tab.grid_selected_row, 0, tab.grid_selected_row, col_count)
}
};
let mut text = String::new();
for row_idx in sr..=er {
if let Some(row_data) = result.rows.get(row_idx) {
if !text.is_empty() {
text.push('\n');
}
let vals: Vec<&str> = (sc..=ec)
.filter_map(|c| row_data.get(c).map(|v| v.as_str()))
.collect();
text.push_str(&vals.join(" "));
}
}
if !text.is_empty() {
copy_to_clipboard(&text);
}
}
fn copy_to_clipboard(text: &str) {
let cmds: &[(&str, &[&str])] = &[
("wl-copy", &[]),
("xclip", &["-selection", "clipboard"]),
("xsel", &["--clipboard", "--input"]),
];
for (cmd, args) in cmds {
if let Ok(mut child) = std::process::Command::new(cmd)
.args(*args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
return;
}
}
}
fn handle_tab_package_list(state: &mut AppState, key: KeyEvent) -> Action {
let tab_idx = state.active_tab_idx;
if tab_idx >= state.tabs.len() {
return Action::None;
}
let tab = &mut state.tabs[tab_idx];
let list_len = match &tab.active_sub_view {
Some(SubView::PackageFunctions) => tab.package_functions.len(),
Some(SubView::PackageProcedures) => tab.package_procedures.len(),
_ => 0,
};
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
if list_len > 0 && tab.package_list_cursor + 1 < list_len {
tab.package_list_cursor += 1;
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
if tab.package_list_cursor > 0 {
tab.package_list_cursor -= 1;
}
Action::Render
}
KeyCode::Char('g') => {
tab.package_list_cursor = 0;
Action::Render
}
KeyCode::Char('G') => {
if list_len > 0 {
tab.package_list_cursor = list_len - 1;
}
Action::Render
}
KeyCode::Enter | KeyCode::Char('l') => {
let selected_name = match &tab.active_sub_view {
Some(SubView::PackageFunctions) => {
tab.package_functions.get(tab.package_list_cursor).cloned()
}
Some(SubView::PackageProcedures) => {
tab.package_procedures.get(tab.package_list_cursor).cloned()
}
_ => None,
};
if let Some(name) = selected_name {
tab.active_sub_view = Some(SubView::PackageDeclaration);
if let Some(editor) = tab.decl_editor.as_mut() {
editor.search.pattern = name;
editor.search.forward = true;
editor.cursor_row = 0;
editor.cursor_col = 0;
editor.jump_to_next_match();
}
}
Action::Render
}
_ => Action::None,
}
}
fn resolve_leader_submenu(
state: &mut AppState,
key_code: KeyCode,
expected: char,
action: Action,
) -> Option<Action> {
state.leader_leader_pending = false;
state.leader_b_pending = false;
state.leader_w_pending = false;
state.leader_s_pending = false;
state.leader_pending = false;
state.leader_pressed_at = None;
Some(if let KeyCode::Char(c) = key_code {
if c == expected {
action
} else {
Action::Render
}
} else {
Action::Render
})
}
fn handle_global_leader(state: &mut AppState, key: KeyEvent) -> Option<Action> {
if state.leader_leader_pending {
let action = state
.active_tab()
.filter(|tab| {
matches!(
tab.kind,
TabKind::Package { .. } | TabKind::Function { .. } | TabKind::Procedure { .. }
)
})
.map(|tab| Action::CompileToDb { tab_id: tab.id })
.unwrap_or(Action::Render);
return resolve_leader_submenu(state, key.code, 's', action);
}
if state.leader_s_pending {
state.leader_s_pending = false;
state.leader_b_pending = false;
state.leader_w_pending = false;
state.leader_pending = false;
state.leader_pressed_at = None;
if let KeyCode::Char('s') = key.code
&& let Some(tab) = state.active_tab_mut()
&& matches!(tab.kind, TabKind::Script { .. })
&& let Some(editor) = tab.active_editor_mut()
{
let template = "SELECT\n *\nFROM ";
editor.save_undo();
let row = editor.cursor_row;
let col = editor.cursor_col;
let line = editor.lines.get(row).cloned().unwrap_or_default();
let before = &line[..col.min(line.len())];
let after = &line[col.min(line.len())..];
let tpl_lines: Vec<&str> = template.lines().collect();
let mut new_lines = Vec::new();
new_lines.push(format!("{before}{}", tpl_lines[0]));
for tpl_line in &tpl_lines[1..tpl_lines.len() - 1] {
new_lines.push((*tpl_line).to_string());
}
let last_tpl = tpl_lines.last().unwrap_or(&"");
new_lines.push(format!("{last_tpl}{after}"));
editor.lines[row] = new_lines[0].clone();
for (i, nl) in new_lines[1..].iter().enumerate() {
editor.lines.insert(row + 1 + i, nl.clone());
}
editor.cursor_row = row + tpl_lines.len() - 1;
editor.cursor_col = last_tpl.len();
editor.mode = vimltui::VimMode::Insert;
}
return Some(Action::Render);
}
if state.leader_b_pending {
return resolve_leader_submenu(state, key.code, 'd', Action::CloseTab);
}
if state.leader_w_pending {
return resolve_leader_submenu(state, key.code, 'd', Action::CloseResultTab);
}
if state.leader_pending {
state.leader_pending = false;
state.leader_pressed_at = None;
return Some(match key.code {
KeyCode::Char(c) if c == vimltui::LEADER_KEY => {
state.leader_leader_pending = true;
Action::Render
}
KeyCode::Char('b') => {
state.leader_b_pending = true;
Action::Render
}
KeyCode::Char('w') => {
state.leader_w_pending = true;
Action::Render
}
KeyCode::Char('s') => {
state.leader_s_pending = true;
Action::Render
}
KeyCode::Char('c') => Action::OpenScriptConnPicker,
KeyCode::Char('t') => Action::OpenThemePicker,
KeyCode::Enter => {
if let Some(tab) = state.active_tab_mut() {
let tab_id = tab.id;
if matches!(tab.kind, TabKind::Script { .. })
&& let Some(editor) = tab.active_editor_mut()
{
let (query, start_line) =
if matches!(editor.mode, vimltui::VimMode::Visual(_)) {
let q = editor.selected_text().unwrap_or_default();
let sl = editor
.visual_anchor
.map(|(r, _)| r.min(editor.cursor_row))
.unwrap_or(editor.cursor_row);
editor.mode = vimltui::VimMode::Normal;
editor.visual_anchor = None;
(q, sl)
} else {
query_block_at_cursor(&editor.lines, editor.cursor_row)
};
if !query.trim().is_empty() {
return Some(maybe_prompt_bind_vars(
state, tab_id, query, start_line, false,
));
}
}
}
Action::Render
}
KeyCode::Char('/') => {
if let Some(tab) = state.active_tab_mut() {
let tab_id = tab.id;
if matches!(tab.kind, TabKind::Script { .. })
&& let Some(editor) = tab.active_editor_mut()
{
let (query, start_line) =
if matches!(editor.mode, vimltui::VimMode::Visual(_)) {
let q = editor.selected_text().unwrap_or_default();
let sl = editor
.visual_anchor
.map(|(r, _)| r.min(editor.cursor_row))
.unwrap_or(editor.cursor_row);
editor.mode = vimltui::VimMode::Normal;
editor.visual_anchor = None;
(q, sl)
} else {
query_block_at_cursor(&editor.lines, editor.cursor_row)
};
if !query.trim().is_empty() {
return Some(maybe_prompt_bind_vars(
state, tab_id, query, start_line, true,
));
}
}
}
Action::Render
}
_ => Action::Render,
});
}
if let KeyCode::Char(c) = key.code
&& c == vimltui::LEADER_KEY
&& !key.modifiers.contains(KeyModifiers::CONTROL)
{
state.leader_pending = true;
state.leader_pressed_at = Some(std::time::Instant::now());
return Some(Action::Render);
}
None
}
fn handle_confirm_close(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y') => {
state.overlay = None;
Action::ConfirmCloseYes
}
KeyCode::Char('n') => {
state.overlay = None;
Action::ConfirmCloseNo
}
KeyCode::Esc | KeyCode::Char('q') => {
state.overlay = None;
Action::Render
}
_ => Action::None,
}
}
fn handle_confirm_quit(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
state.overlay = None;
Action::Quit
}
KeyCode::Esc | KeyCode::Char('n') => {
state.overlay = None;
Action::Render
}
_ => Action::None,
}
}
fn handle_save_script_name(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.scripts_save_name = None;
state.overlay = None;
Action::Render
}
KeyCode::Enter => {
if let Some(name) = state.scripts_save_name.take() {
state.overlay = None;
if !name.is_empty() {
return Action::SaveScriptAs { name };
}
}
Action::Render
}
KeyCode::Backspace => {
if let Some(ref mut buf) = state.scripts_save_name {
buf.pop();
}
Action::Render
}
KeyCode::Char(c) => {
if let Some(ref mut buf) = state.scripts_save_name {
buf.push(c);
}
Action::Render
}
_ => Action::None,
}
}
fn handle_scripts_panel(state: &mut AppState, key: KeyEvent) -> Action {
match &state.scripts_mode {
ScriptsMode::ConfirmDelete { .. } => return handle_scripts_confirm(state, key),
ScriptsMode::Insert { .. } => return handle_scripts_insert(state, key),
ScriptsMode::Rename { .. } => return handle_scripts_rename_mode(state, key),
ScriptsMode::PendingD => return handle_scripts_pending_d(state, key),
ScriptsMode::PendingY => return handle_scripts_pending_y(state, key),
ScriptsMode::Normal => {}
}
let visible = state.visible_scripts();
let count = visible.len();
let selected: Option<(usize, ScriptNode)> = visible
.get(state.scripts_cursor)
.map(|(idx, node)| (*idx, (*node).clone()));
drop(visible);
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
if count > 0 && state.scripts_cursor + 1 < count {
state.scripts_cursor += 1;
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
if state.scripts_cursor > 0 {
state.scripts_cursor -= 1;
}
Action::Render
}
KeyCode::Char('g') => {
state.scripts_cursor = 0;
state.scripts_offset = 0;
Action::Render
}
KeyCode::Char('G') => {
if count > 0 {
state.scripts_cursor = count - 1;
}
Action::Render
}
KeyCode::Enter | KeyCode::Char('l') => {
if let Some((idx, node)) = selected {
match node {
ScriptNode::Collection { .. } => {
if let Some(ScriptNode::Collection { expanded, .. }) =
state.scripts_tree.get_mut(idx)
{
*expanded = !*expanded;
}
Action::Render
}
ScriptNode::Script { file_path, .. } => {
let name = file_path
.strip_suffix(".sql")
.unwrap_or(&file_path)
.to_string();
Action::OpenScript { name }
}
}
} else {
Action::None
}
}
KeyCode::Char('h') => {
if let Some((idx, node)) = selected {
match node {
ScriptNode::Collection { .. } => {
if let Some(ScriptNode::Collection { expanded, .. }) =
state.scripts_tree.get_mut(idx)
{
*expanded = false;
}
}
ScriptNode::Script {
collection: Some(coll_name),
..
} => {
for tnode in state.scripts_tree.iter_mut() {
if let ScriptNode::Collection { name, expanded } = tnode
&& *name == coll_name
{
*expanded = false;
break;
}
}
let vis = state.visible_scripts();
for (vi, (_, vnode)) in vis.iter().enumerate() {
if let ScriptNode::Collection { name, .. } = vnode
&& *name == coll_name
{
state.scripts_cursor = vi;
break;
}
}
}
_ => {}
}
}
Action::Render
}
KeyCode::Char('d') => {
state.scripts_mode = ScriptsMode::PendingD;
Action::Render
}
KeyCode::Char('y') => {
state.scripts_mode = ScriptsMode::PendingY;
Action::Render
}
KeyCode::Char('p') => {
if let Some(from) = state.scripts_yank.clone() {
let to_collection = state.current_collection();
state.scripts_yank = None;
return Action::ScriptOp {
op: ScriptOperation::Move {
from,
to_collection,
},
};
}
Action::None
}
KeyCode::Char('i') | KeyCode::Char('o') => {
state.scripts_mode = ScriptsMode::Insert { buf: String::new() };
Action::Render
}
KeyCode::Char('c') => {
if let Some((_, node)) = selected {
let (buf, path) = match node {
ScriptNode::Collection { name, .. } => (format!("{name}/"), name),
ScriptNode::Script {
name, file_path, ..
} => (name, file_path),
};
state.scripts_mode = ScriptsMode::Rename {
buf,
original_path: path,
};
}
Action::Render
}
_ => Action::None,
}
}
fn handle_scripts_confirm(state: &mut AppState, key: KeyEvent) -> Action {
let path = if let ScriptsMode::ConfirmDelete { path } = &state.scripts_mode {
path.clone()
} else {
return Action::None;
};
match key.code {
KeyCode::Char('y') | KeyCode::Enter => {
state.scripts_mode = ScriptsMode::Normal;
if !path.contains('.') {
Action::ScriptOp {
op: ScriptOperation::DeleteCollection { name: path },
}
} else {
Action::ScriptOp {
op: ScriptOperation::Delete { path },
}
}
}
_ => {
state.scripts_mode = ScriptsMode::Normal;
Action::Render
}
}
}
fn handle_scripts_insert(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.scripts_mode = ScriptsMode::Normal;
Action::Render
}
KeyCode::Enter => {
let buf = if let ScriptsMode::Insert { buf } = &state.scripts_mode {
buf.clone()
} else {
return Action::None;
};
state.scripts_mode = ScriptsMode::Normal;
if buf.is_empty() {
return Action::Render;
}
let in_collection = state.current_collection();
Action::ScriptOp {
op: ScriptOperation::Create {
name: buf,
in_collection,
},
}
}
KeyCode::Backspace => {
if let ScriptsMode::Insert { buf } = &mut state.scripts_mode {
buf.pop();
}
Action::Render
}
KeyCode::Char(c) => {
if let ScriptsMode::Insert { buf } = &mut state.scripts_mode {
buf.push(c);
}
Action::Render
}
_ => Action::None,
}
}
fn handle_scripts_rename_mode(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.scripts_mode = ScriptsMode::Normal;
Action::Render
}
KeyCode::Enter => {
let (buf, original_path) =
if let ScriptsMode::Rename { buf, original_path } = &state.scripts_mode {
(buf.clone(), original_path.clone())
} else {
return Action::None;
};
state.scripts_mode = ScriptsMode::Normal;
if buf.is_empty() {
return Action::Render;
}
if buf.ends_with('/') {
let new_name = buf.trim_end_matches('/').to_string();
Action::ScriptOp {
op: ScriptOperation::RenameCollection {
old_name: original_path,
new_name,
},
}
} else {
Action::ScriptOp {
op: ScriptOperation::Rename {
old_path: original_path,
new_name: buf,
},
}
}
}
KeyCode::Backspace => {
if let ScriptsMode::Rename { buf, .. } = &mut state.scripts_mode {
buf.pop();
}
Action::Render
}
KeyCode::Char(c) => {
if let ScriptsMode::Rename { buf, .. } = &mut state.scripts_mode {
buf.push(c);
}
Action::Render
}
_ => Action::None,
}
}
fn handle_scripts_pending_d(state: &mut AppState, key: KeyEvent) -> Action {
state.scripts_mode = ScriptsMode::Normal;
if key.code == KeyCode::Char('d') {
let visible = state.visible_scripts();
let selected = visible
.get(state.scripts_cursor)
.map(|(_, node)| (*node).clone());
drop(visible);
if let Some(node) = selected {
let path = match node {
ScriptNode::Collection { name, .. } => name,
ScriptNode::Script { file_path, .. } => file_path,
};
state.scripts_mode = ScriptsMode::ConfirmDelete { path };
}
}
Action::Render
}
fn handle_scripts_pending_y(state: &mut AppState, key: KeyEvent) -> Action {
state.scripts_mode = ScriptsMode::Normal;
if key.code == KeyCode::Char('y') {
let visible = state.visible_scripts();
let selected = visible
.get(state.scripts_cursor)
.map(|(_, node)| (*node).clone());
drop(visible);
if let Some(ScriptNode::Script { file_path, .. }) = selected {
state.scripts_yank = Some(file_path);
}
}
Action::Render
}
fn handle_group_rename(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.group_renaming = None;
state.group_rename_buf.clear();
Action::Render
}
KeyCode::Enter => {
let new_name = state.group_rename_buf.trim().to_string();
if let Some(old_name) = state.group_renaming.take() {
state.group_rename_buf.clear();
if !new_name.is_empty() && new_name != old_name {
for node in &mut state.tree {
if let TreeNode::Group { name, .. } = node
&& *name == old_name
{
*name = new_name.clone();
}
}
for conn in &mut state.saved_connections {
if conn.group == old_name {
conn.group = new_name.clone();
}
}
if let Ok(store) = crate::core::storage::ConnectionStore::new() {
let _ = store.save(&state.saved_connections, "");
let _ = store.save_groups(&persist_group_names(state));
}
state.status_message = format!("Group renamed: '{old_name}' → '{new_name}'");
}
}
Action::Render
}
KeyCode::Backspace => {
state.group_rename_buf.pop();
Action::Render
}
KeyCode::Char(c) => {
state.group_rename_buf.push(c);
Action::Render
}
_ => Action::None,
}
}
fn handle_group_create(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.group_creating = false;
state.group_rename_buf.clear();
Action::Render
}
KeyCode::Enter => {
let name = state.group_rename_buf.trim().to_string();
state.group_creating = false;
state.group_rename_buf.clear();
if !name.is_empty() {
let exists = state
.tree
.iter()
.any(|n| matches!(n, TreeNode::Group { name: gn, .. } if gn == &name));
if exists {
state.status_message = format!("Group '{name}' already exists");
} else {
state.tree.push(TreeNode::Group {
name: name.clone(),
expanded: true,
});
if let Ok(store) = crate::core::storage::ConnectionStore::new() {
let _ = store.save_groups(&persist_group_names(state));
}
state.status_message = format!("Group '{name}' created");
}
}
Action::Render
}
KeyCode::Backspace => {
state.group_rename_buf.pop();
Action::Render
}
KeyCode::Char(c) => {
state.group_rename_buf.push(c);
Action::Render
}
_ => Action::None,
}
}
fn handle_filter_key(state: &mut AppState) -> Action {
if let Some(idx) = state.selected_tree_index() {
let conn_prefix = state.connection_for_tree_idx(idx).unwrap_or("").to_string();
match &state.tree[idx] {
TreeNode::Group { .. } => {}
TreeNode::Connection { .. } | TreeNode::Schema { .. } => {
let schemas = state.schema_names_for_conn(&conn_prefix);
if !schemas.is_empty() {
let key = format!("{conn_prefix}::schemas");
state.object_filter.open_for(&key, schemas);
state.overlay = Some(Overlay::ObjectFilter);
}
}
TreeNode::Category { schema, kind, .. } => {
let base_key = kind.filter_key(schema);
let key = format!("{conn_prefix}::{base_key}");
let items = state.leaves_under_category(idx);
if !items.is_empty() {
state.object_filter.open_for(&key, items);
state.overlay = Some(Overlay::ObjectFilter);
}
}
TreeNode::Empty => {}
TreeNode::Leaf { schema, kind, .. } => {
let base_key = match kind {
LeafKind::Table => format!("{schema}.Tables"),
LeafKind::View => format!("{schema}.Views"),
LeafKind::MaterializedView => format!("{schema}.MaterializedViews"),
LeafKind::Index => format!("{schema}.Indexes"),
LeafKind::Sequence => format!("{schema}.Sequences"),
LeafKind::Type => format!("{schema}.Types"),
LeafKind::Trigger => format!("{schema}.Triggers"),
LeafKind::Package => format!("{schema}.Packages"),
LeafKind::Procedure => format!("{schema}.Procedures"),
LeafKind::Function => format!("{schema}.Functions"),
LeafKind::Event => format!("{schema}.Events"),
};
let cat_key = format!("{conn_prefix}::{base_key}");
let mut walk = idx;
while walk > 0 {
walk -= 1;
if matches!(&state.tree[walk], TreeNode::Category { .. }) {
let items = state.leaves_under_category(walk);
if !items.is_empty() {
state.object_filter.open_for(&cat_key, items);
state.overlay = Some(Overlay::ObjectFilter);
}
break;
}
}
}
}
} else if !state.tree.is_empty() {
let schemas = state.all_schema_names();
if !schemas.is_empty() {
state.object_filter.open_for("schemas", schemas);
state.overlay = Some(Overlay::ObjectFilter);
}
}
Action::Render
}
fn handle_object_filter(state: &mut AppState, key: KeyEvent) -> Action {
if state.object_filter.search_active {
return handle_object_filter_search(state, key);
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
state.overlay = None;
Action::SaveSchemaFilter
}
KeyCode::Char('j') | KeyCode::Down => {
state.object_filter.move_down();
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.object_filter.move_up();
Action::Render
}
KeyCode::Char('g') => {
state.object_filter.go_top();
Action::Render
}
KeyCode::Char('G') => {
state.object_filter.go_bottom();
Action::Render
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = state.object_filter.visible_height / 2;
let count = state.object_filter.display_list().len();
state.object_filter.cursor =
(state.object_filter.cursor + half).min(count.saturating_sub(1));
state.object_filter.offset = state
.object_filter
.cursor
.saturating_sub(state.object_filter.visible_height / 2);
Action::Render
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let half = state.object_filter.visible_height / 2;
state.object_filter.cursor = state.object_filter.cursor.saturating_sub(half);
state.object_filter.offset = state
.object_filter
.cursor
.saturating_sub(state.object_filter.visible_height / 2);
Action::Render
}
KeyCode::Char(' ') => {
state.object_filter.toggle_at_cursor();
Action::SaveSchemaFilter
}
KeyCode::Char('a') => {
state.object_filter.select_all();
Action::SaveSchemaFilter
}
KeyCode::Char('/') => {
state.object_filter.search_active = true;
state.object_filter.search_query.clear();
state.object_filter.cursor = 0;
state.object_filter.offset = 0;
Action::Render
}
KeyCode::Enter => {
state.overlay = None;
Action::SaveSchemaFilter
}
_ => Action::None,
}
}
fn handle_object_filter_search(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.object_filter.search_active = false;
state.object_filter.search_query.clear();
state.object_filter.cursor = 0;
state.object_filter.offset = 0;
Action::Render
}
KeyCode::Enter => {
state.object_filter.search_active = false;
Action::Render
}
KeyCode::Backspace => {
state.object_filter.search_query.pop();
state.object_filter.cursor = 0;
state.object_filter.offset = 0;
Action::Render
}
KeyCode::Char(c) => {
state.object_filter.search_query.push(c);
state.object_filter.cursor = 0;
state.object_filter.offset = 0;
Action::Render
}
_ => Action::None,
}
}
fn handle_sidebar_search(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.tree_state.search_active = false;
state.tree_state.search_query.clear();
state.tree_state.search_matches.clear();
Action::Render
}
KeyCode::Enter => {
state.tree_state.search_active = false;
Action::Render
}
KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let count = state.visible_tree().len();
state.tree_state.next_match(count);
Action::Render
}
KeyCode::Backspace => {
state.tree_state.search_query.pop();
update_search_and_jump(state);
Action::Render
}
KeyCode::Char(c) => {
state.tree_state.search_query.push(c);
update_search_and_jump(state);
Action::Render
}
_ => Action::None,
}
}
fn handle_connection_dialog(state: &mut AppState, key: KeyEvent) -> Action {
if state.connection_form.show_saved_list {
return handle_saved_connections_list(state, key);
}
if state.connection_form.read_only {
return match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
state.overlay = None;
Action::Render
}
_ => Action::None,
};
}
match key.code {
KeyCode::Esc => {
state.overlay = None;
Action::Render
}
KeyCode::Tab => {
state.connection_form.next_field();
Action::Render
}
KeyCode::BackTab => {
state.connection_form.prev_field();
Action::Render
}
KeyCode::Enter => {
if state.connection_form.name.is_empty() {
state.connection_form.error_message = "Name is required".to_string();
return Action::Render;
}
if state.connection_form.host.is_empty() {
state.connection_form.error_message = "Host is required".to_string();
return Action::Render;
}
if state.connection_form.username.is_empty() {
state.connection_form.error_message = "Username is required".to_string();
return Action::Render;
}
state.connection_form.error_message.clear();
state.connection_form.connecting = true;
Action::Connect
}
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.connection_form.password_visible = !state.connection_form.password_visible;
Action::Render
}
KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.connection_form.cycle_db_type();
Action::Render
}
KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::CONTROL) => {
state.connection_form.cycle_group();
Action::Render
}
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
Action::SaveConnection
}
KeyCode::Char(c) => {
if state.connection_form.selected_field == 1
|| state.connection_form.selected_field == 7
{
return Action::None;
}
state.connection_form.active_field_mut().push(c);
state.connection_form.error_message.clear();
Action::Render
}
KeyCode::Backspace => {
if state.connection_form.selected_field != 1
&& state.connection_form.selected_field != 7
{
state.connection_form.active_field_mut().pop();
}
Action::Render
}
_ => Action::None,
}
}
fn handle_saved_connections_list(state: &mut AppState, key: KeyEvent) -> Action {
let count = state.saved_connections.len();
match key.code {
KeyCode::Esc => {
state.overlay = None;
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
if count > 0 {
state.connection_form.saved_cursor =
(state.connection_form.saved_cursor + 1) % (count + 1);
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
if state.connection_form.saved_cursor == 0 {
state.connection_form.saved_cursor = count;
} else {
state.connection_form.saved_cursor -= 1;
}
Action::Render
}
KeyCode::Enter => {
let cursor = state.connection_form.saved_cursor;
if cursor < count {
let config = state.saved_connections[cursor].clone();
let groups = state.available_groups();
state.connection_form = crate::ui::state::ConnectionFormState::from_config(&config);
state.connection_form.group_options = groups;
state.connection_form.connecting = true;
Action::Connect
} else {
state.connection_form.show_saved_list = false;
Action::Render
}
}
KeyCode::Char('n') => {
state.connection_form.show_saved_list = false;
Action::Render
}
KeyCode::Char('d') => {
let cursor = state.connection_form.saved_cursor;
if cursor < count {
let name = state.saved_connections[cursor].name.clone();
state.saved_connections.remove(cursor);
if let Ok(store) = crate::core::storage::ConnectionStore::new() {
let _ = store.save(&state.saved_connections, "");
}
state.status_message = format!("Connection '{name}' deleted");
if state.connection_form.saved_cursor >= state.saved_connections.len()
&& state.connection_form.saved_cursor > 0
{
state.connection_form.saved_cursor -= 1;
}
if state.saved_connections.is_empty() {
state.connection_form.show_saved_list = false;
}
}
Action::Render
}
_ => Action::None,
}
}
fn handle_conn_menu(state: &mut AppState, key: KeyEvent) -> Action {
use crate::ui::state::ConnMenuAction;
let actions = ConnMenuAction::all();
let count = actions.len();
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
state.overlay = None;
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
state.conn_menu.cursor = (state.conn_menu.cursor + 1) % count;
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.conn_menu.cursor = if state.conn_menu.cursor == 0 {
count - 1
} else {
state.conn_menu.cursor - 1
};
Action::Render
}
KeyCode::Enter => {
let selected = &actions[state.conn_menu.cursor];
let name = state.conn_menu.conn_name.clone();
state.overlay = None;
match selected {
ConnMenuAction::View => {
if let Some(config) = state.saved_connections.iter().find(|c| c.name == name) {
let groups = state.available_groups();
let mut form = crate::ui::state::ConnectionFormState::from_config(config);
form.password = "********".to_string();
form.password_visible = false;
form.read_only = true;
form.group_options = groups;
state.connection_form = form;
state.overlay = Some(Overlay::ConnectionDialog);
}
Action::Render
}
ConnMenuAction::Edit => {
if let Some(config) = state.saved_connections.iter().find(|c| c.name == name) {
let groups = state.available_groups();
state.connection_form =
crate::ui::state::ConnectionFormState::for_edit(config);
state.connection_form.group_options = groups;
state.overlay = Some(Overlay::ConnectionDialog);
}
Action::Render
}
ConnMenuAction::Connect => Action::ConnectByName { name },
ConnMenuAction::Disconnect => Action::DisconnectByName { name },
ConnMenuAction::Restart => Action::ConnectByName { name },
ConnMenuAction::Delete => Action::DeleteConnection { name },
}
}
_ => Action::None,
}
}
fn handle_group_menu(state: &mut AppState, key: KeyEvent) -> Action {
use crate::ui::state::GroupMenuAction;
let actions = GroupMenuAction::all();
let count = actions.len();
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
state.overlay = None;
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
state.group_menu.cursor = (state.group_menu.cursor + 1) % count;
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.group_menu.cursor = if state.group_menu.cursor == 0 {
count - 1
} else {
state.group_menu.cursor - 1
};
Action::Render
}
KeyCode::Enter => {
let selected_idx = state.group_menu.cursor;
let group_name = state.group_menu.group_name.clone();
let is_empty = state.group_menu.is_empty;
state.overlay = None;
match &actions[selected_idx] {
GroupMenuAction::Rename => {
state.group_renaming = Some(group_name.clone());
state.group_rename_buf = group_name;
Action::Render
}
GroupMenuAction::Delete => {
if !is_empty {
state.status_message = "Cannot delete group with connections".to_string();
return Action::Render;
}
state.tree.retain(
|n| !matches!(n, TreeNode::Group { name, .. } if name == &group_name),
);
state.status_message = format!("Group '{group_name}' deleted");
if let Ok(store) = crate::core::storage::ConnectionStore::new() {
let _ = store.save_groups(&persist_group_names(state));
}
Action::Render
}
GroupMenuAction::NewGroup => {
state.group_creating = true;
state.group_rename_buf.clear();
Action::Render
}
}
}
_ => Action::None,
}
}
fn handle_help_overlay(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc | KeyCode::Char('?') | KeyCode::Char('q') => {
state.overlay = None;
Action::Render
}
_ => Action::None,
}
}
fn update_search_and_jump(state: &mut AppState) {
let query = state.tree_state.search_query.to_lowercase();
let visible = state.visible_tree();
let mut matches = Vec::new();
for (vis_idx, (_, node)) in visible.iter().enumerate() {
if !query.is_empty() && node.display_name().to_lowercase().contains(&query) {
matches.push(vis_idx);
}
}
let count = visible.len();
drop(visible);
state.tree_state.search_matches = matches;
state.tree_state.search_match_idx = 0;
if let Some(&first) = state.tree_state.search_matches.first() {
state.tree_state.cursor = first;
state.tree_state.center_scroll(count);
}
}
fn handle_sidebar(state: &mut AppState, key: KeyEvent) -> Action {
if state.group_renaming.is_some() {
return handle_group_rename(state, key);
}
if state.group_creating {
return handle_group_create(state, key);
}
let visible_count = state.visible_tree().len();
if visible_count == 0 {
return Action::None;
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('d') => {
state.tree_state.half_page_down(visible_count);
return Action::Render;
}
KeyCode::Char('u') => {
state.tree_state.half_page_up(visible_count);
return Action::Render;
}
_ => {}
}
}
if key.code != KeyCode::Char('d') {
state.tree_state.pending_d = false;
}
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
state.tree_state.move_down(visible_count);
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.tree_state.move_up();
Action::Render
}
KeyCode::Char('g') => {
state.tree_state.go_top();
Action::Render
}
KeyCode::Char('G') => {
state.tree_state.go_bottom(visible_count);
Action::Render
}
KeyCode::Char('l') | KeyCode::Enter => {
if let Some(idx) = state.selected_tree_index() {
handle_tree_action(state, idx)
} else {
Action::None
}
}
KeyCode::Char('h') | KeyCode::Left => {
if let Some(idx) = state.selected_tree_index() {
if state.tree[idx].is_expanded() {
state.tree[idx].toggle_expand();
} else {
let child_depth = state.tree[idx].depth();
if child_depth > 0 {
let mut walk = idx;
while walk > 0 {
walk -= 1;
if state.tree[walk].depth() < child_depth {
if state.tree[walk].is_expanded() {
state.tree[walk].toggle_expand();
}
let vis_info = {
let visible = state.visible_tree();
visible
.iter()
.position(|(vi, _)| *vi == walk)
.map(|p| (p, visible.len()))
};
if let Some((vis_pos, vis_len)) = vis_info {
state.tree_state.cursor = vis_pos;
state.tree_state.adjust_scroll(vis_len);
}
break;
}
}
}
}
}
Action::Render
}
KeyCode::Char('d') => {
if state.tree_state.pending_d {
state.tree_state.pending_d = false;
if let Some(idx) = state.selected_tree_index() {
match &state.tree[idx] {
TreeNode::Connection { name, .. } => {
return Action::DeleteConnection { name: name.clone() };
}
TreeNode::Leaf {
name, schema, kind, ..
} if matches!(
kind,
LeafKind::Table | LeafKind::View | LeafKind::Package
) =>
{
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
LeafKind::Package => "PACKAGE",
_ => unreachable!(),
};
let conn_name = find_conn_name_for(state, idx);
state.sidebar_pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: schema.clone(),
name: name.clone(),
obj_type: obj_type.to_string(),
conn_name,
});
state.overlay = Some(Overlay::ConfirmDropObject);
return Action::Render;
}
_ => {}
}
}
Action::Render
} else {
state.tree_state.pending_d = true;
Action::Render
}
}
KeyCode::Char('r') => {
if let Some(idx) = state.selected_tree_index() {
match &state.tree[idx] {
TreeNode::Connection { name, .. } => {
state.sidebar_rename_buf = name.clone();
state.sidebar_pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: String::new(),
name: name.clone(),
obj_type: "CONNECTION".to_string(),
conn_name: name.clone(),
});
state.overlay = Some(Overlay::RenameObject);
return Action::Render;
}
TreeNode::Leaf {
name, schema, kind, ..
} if matches!(kind, LeafKind::Table | LeafKind::View) => {
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
_ => unreachable!(),
};
let conn_name = find_conn_name_for(state, idx);
state.sidebar_rename_buf = name.clone();
state.sidebar_pending_action =
Some(crate::ui::state::PendingObjectAction {
schema: schema.clone(),
name: name.clone(),
obj_type: obj_type.to_string(),
conn_name,
});
state.overlay = Some(Overlay::RenameObject);
return Action::Render;
}
_ => {}
}
}
Action::Render
}
KeyCode::Char('y') => {
if let Some(idx) = state.selected_tree_index() {
let mut walk = idx;
loop {
if let TreeNode::Connection { name, .. } = &state.tree[walk] {
state.sidebar_yank_conn = Some(name.clone());
state.status_message = format!("Yanked connection: {name}");
break;
}
if walk == 0 {
break;
}
walk -= 1;
}
}
Action::Render
}
KeyCode::Char('p') => {
if let Some(ref source) = state.sidebar_yank_conn.clone() {
let group = if let Some(idx) = state.selected_tree_index() {
let mut walk = idx;
loop {
if let TreeNode::Group { name, .. } = &state.tree[walk] {
break name.clone();
}
if walk == 0 {
break "Default".to_string();
}
walk -= 1;
}
} else {
"Default".to_string()
};
return Action::DuplicateConnection {
source_name: source.clone(),
target_group: group,
};
}
Action::Render
}
KeyCode::Char('o') | KeyCode::Char('i') => {
if let Some(idx) = state.selected_tree_index() {
match &state.tree[idx] {
TreeNode::Connection { .. } | TreeNode::Group { .. } => {
state.connection_form = crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
return Action::Render;
}
TreeNode::Category { schema, kind, .. } => {
let obj_type = match kind {
crate::ui::state::CategoryKind::Tables => "TABLE",
crate::ui::state::CategoryKind::Views => "VIEW",
crate::ui::state::CategoryKind::Packages => "PACKAGE",
_ => return Action::Render,
};
let conn_name = find_conn_name_for(state, idx);
return Action::CreateFromTemplate {
conn_name,
schema: schema.clone(),
obj_type: obj_type.to_string(),
};
}
TreeNode::Leaf { schema, kind, .. } => {
let obj_type = match kind {
LeafKind::Table => "TABLE",
LeafKind::View => "VIEW",
LeafKind::Package => "PACKAGE",
_ => return Action::Render,
};
let conn_name = find_conn_name_for(state, idx);
return Action::CreateFromTemplate {
conn_name,
schema: schema.clone(),
obj_type: obj_type.to_string(),
};
}
_ => {
state.connection_form = crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
return Action::Render;
}
}
} else {
state.connection_form = crate::ui::state::ConnectionFormState::new();
state.overlay = Some(Overlay::ConnectionDialog);
}
Action::Render
}
KeyCode::Char('m') => {
if let Some(idx) = state.selected_tree_index() {
if let TreeNode::Group { name, .. } = &state.tree[idx] {
let group_name = name.clone();
let has_children = idx + 1 < state.tree.len()
&& state.tree[idx + 1].depth() > state.tree[idx].depth();
state.group_menu.group_name = group_name;
state.group_menu.cursor = 0;
state.group_menu.is_empty = !has_children;
state.overlay = Some(Overlay::GroupMenu);
return Action::Render;
}
let mut walk = idx;
loop {
if let TreeNode::Connection { name, status, .. } = &state.tree[walk] {
let conn_name = name.clone();
state.conn_menu.conn_name = conn_name;
state.conn_menu.cursor = 0;
state.conn_menu.is_connected =
*status == crate::ui::state::ConnStatus::Connected;
state.overlay = Some(Overlay::ConnectionMenu);
return Action::Render;
}
if walk == 0 {
break;
}
walk -= 1;
}
}
Action::None
}
KeyCode::Char('/') => {
state.tree_state.search_active = true;
state.tree_state.search_query.clear();
state.tree_state.search_matches.clear();
Action::Render
}
_ => Action::None,
}
}
fn find_conn_name_for(state: &AppState, mut idx: usize) -> String {
loop {
if let TreeNode::Connection { name, .. } = &state.tree[idx] {
return name.clone();
}
if idx == 0 {
break;
}
idx -= 1;
}
state.connection_name.clone().unwrap_or_default()
}
fn handle_tree_action(state: &mut AppState, idx: usize) -> Action {
if idx >= state.tree.len() {
return Action::None;
}
let node = &state.tree[idx];
match node {
TreeNode::Connection { expanded, name, .. } if !expanded => {
let conn_name = name.clone();
state.tree[idx].toggle_expand();
Action::LoadSchemas { conn_name }
}
TreeNode::Schema { expanded, name, .. } if !expanded => {
let schema = name.clone();
state.tree[idx].toggle_expand();
let has_children =
idx + 1 < state.tree.len() && state.tree[idx + 1].depth() > state.tree[idx].depth();
if !has_children {
insert_categories(state, idx, &schema);
}
Action::Render
}
TreeNode::Category {
expanded,
schema,
label,
..
} if !expanded => {
let schema = schema.clone();
let label = label.clone();
state.tree[idx].toggle_expand();
Action::LoadChildren {
schema,
kind: label,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Table | LeafKind::View | LeafKind::MaterializedView,
..
} => {
let schema = schema.clone();
let table = name.clone();
let conn_name = find_conn_name_for(state, idx);
state.current_schema = Some(schema.clone());
let tab_id = state.open_or_focus_tab(TabKind::Table {
conn_name,
schema: schema.clone(),
table: table.clone(),
});
Action::LoadTableData {
tab_id,
schema,
table,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Package,
..
} => {
let schema = schema.clone();
let pkg_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Package {
conn_name,
schema: schema.clone(),
name: pkg_name.clone(),
});
Action::LoadPackageContent {
tab_id,
schema,
name: pkg_name,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Function,
..
} => {
let schema = schema.clone();
let func_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Function {
conn_name,
schema: schema.clone(),
name: func_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: func_name,
obj_type: "FUNCTION".to_string(),
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Procedure,
..
} => {
let schema = schema.clone();
let proc_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Procedure {
conn_name,
schema: schema.clone(),
name: proc_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: proc_name,
obj_type: "PROCEDURE".to_string(),
}
}
TreeNode::Leaf {
schema, name, kind, ..
} if matches!(kind, LeafKind::Index | LeafKind::Sequence | LeafKind::Event) => {
let schema = schema.clone();
let obj_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let obj_type = match kind {
LeafKind::Index => "INDEX",
LeafKind::Sequence => "SEQUENCE",
LeafKind::Event => "EVENT",
_ => unreachable!(),
};
let tab_id = state.open_or_focus_tab(TabKind::Function {
conn_name,
schema: schema.clone(),
name: obj_name.clone(),
});
Action::LoadSourceCode {
tab_id,
schema,
name: obj_name,
obj_type: obj_type.to_string(),
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Type,
..
} => {
let schema = schema.clone();
let type_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::DbType {
conn_name,
schema: schema.clone(),
name: type_name.clone(),
});
Action::LoadTypeInfo {
tab_id,
schema,
name: type_name,
}
}
TreeNode::Leaf {
schema,
name,
kind: LeafKind::Trigger,
..
} => {
let schema = schema.clone();
let trigger_name = name.clone();
let conn_name = find_conn_name_for(state, idx);
let tab_id = state.open_or_focus_tab(TabKind::Trigger {
conn_name,
schema: schema.clone(),
name: trigger_name.clone(),
});
Action::LoadTriggerInfo {
tab_id,
schema,
name: trigger_name,
}
}
_ => {
state.tree[idx].toggle_expand();
Action::Render
}
}
}
fn insert_categories(state: &mut AppState, parent_idx: usize, schema: &str) {
use crate::ui::state::CategoryKind;
let categories: Vec<(&str, CategoryKind)> = match state.db_type {
Some(DatabaseType::Oracle) => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Materialized Views", CategoryKind::MaterializedViews),
("Indexes", CategoryKind::Indexes),
("Sequences", CategoryKind::Sequences),
("Types", CategoryKind::Types),
("Triggers", CategoryKind::Triggers),
("Packages", CategoryKind::Packages),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
Some(DatabaseType::MySQL) => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Indexes", CategoryKind::Indexes),
("Triggers", CategoryKind::Triggers),
("Events", CategoryKind::Events),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
Some(DatabaseType::PostgreSQL) | None => vec![
("Tables", CategoryKind::Tables),
("Views", CategoryKind::Views),
("Materialized Views", CategoryKind::MaterializedViews),
("Indexes", CategoryKind::Indexes),
("Sequences", CategoryKind::Sequences),
("Triggers", CategoryKind::Triggers),
("Procedures", CategoryKind::Procedures),
("Functions", CategoryKind::Functions),
],
};
let insert_pos = parent_idx + 1;
for (i, (label, kind)) in categories.into_iter().enumerate() {
state.tree.insert(
insert_pos + i,
TreeNode::Category {
label: label.to_string(),
schema: schema.to_string(),
kind,
expanded: false,
},
);
}
}
fn handle_script_conn_picker(state: &mut AppState, key: KeyEvent) -> Action {
use crate::ui::state::PickerItem;
let picker = match &mut state.script_conn_picker {
Some(p) => p,
None => {
state.overlay = None;
return Action::Render;
}
};
let count = picker.visible_count();
match key.code {
KeyCode::Esc => {
state.overlay = None;
state.script_conn_picker = None;
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
if count > 0 {
picker.cursor = (picker.cursor + 1).min(count - 1);
}
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
picker.cursor = picker.cursor.saturating_sub(1);
Action::Render
}
KeyCode::Enter | KeyCode::Char('l') => {
let items = picker.visible_items();
match items.get(picker.cursor) {
Some(PickerItem::Active(name)) | Some(PickerItem::Other(name)) => {
let conn_name = name.clone();
state.overlay = None;
state.script_conn_picker = None;
Action::SetScriptConnection { conn_name }
}
Some(PickerItem::OthersHeader) => {
picker.others_expanded = !picker.others_expanded;
Action::Render
}
None => {
state.overlay = None;
state.script_conn_picker = None;
Action::Render
}
}
}
_ => Action::None,
}
}
fn handle_theme_picker(state: &mut AppState, key: KeyEvent) -> Action {
use crate::ui::theme::THEME_NAMES;
let count = THEME_NAMES.len();
match key.code {
KeyCode::Esc => {
state.overlay = None;
Action::Render
}
KeyCode::Char('j') | KeyCode::Down => {
state.theme_picker.cursor = (state.theme_picker.cursor + 1).min(count - 1);
Action::Render
}
KeyCode::Char('k') | KeyCode::Up => {
state.theme_picker.cursor = state.theme_picker.cursor.saturating_sub(1);
Action::Render
}
KeyCode::Enter => {
let name = THEME_NAMES[state.theme_picker.cursor].to_string();
state.overlay = None;
Action::SetTheme { name }
}
_ => Action::None,
}
}
fn extract_bind_variables(query: &str) -> Vec<String> {
let mut vars = Vec::new();
let mut seen = std::collections::HashSet::new();
let bytes = query.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\'' {
i += 1;
while i < bytes.len() && bytes[i] != b'\'' {
i += 1;
}
i += 1;
continue;
}
if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'-' {
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
continue;
}
if bytes[i] == b':'
&& i + 1 < bytes.len()
&& bytes[i + 1].is_ascii_alphabetic()
&& (i == 0 || bytes[i - 1] != b':')
{
let start = i + 1;
let mut end = start;
while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') {
end += 1;
}
let name = &query[start..end];
if !name.is_empty() && seen.insert(name.to_string()) {
vars.push(name.to_string());
}
i = end;
continue;
}
i += 1;
}
vars
}
fn maybe_prompt_bind_vars(
state: &mut AppState,
tab_id: TabId,
query: String,
start_line: usize,
new_tab: bool,
) -> Action {
let vars = extract_bind_variables(&query);
if vars.is_empty() {
if new_tab {
Action::ExecuteQueryNewTab {
tab_id,
query,
start_line,
}
} else {
Action::ExecuteQuery {
tab_id,
query,
start_line,
}
}
} else {
let saved = crate::ui::app::load_bind_variable_values();
let variables = vars
.into_iter()
.map(|name| {
let value = saved.get(&name).cloned().unwrap_or_default();
(name, value)
})
.collect();
state.bind_variables = Some(crate::ui::state::BindVariablesState {
variables,
selected_idx: 0,
query,
tab_id,
start_line,
new_tab,
});
state.overlay = Some(Overlay::BindVariables);
Action::Render
}
}
fn handle_bind_variables(state: &mut AppState, key: KeyEvent) -> Action {
match key.code {
KeyCode::Esc => {
state.bind_variables = None;
state.overlay = None;
Action::Render
}
KeyCode::Tab => {
if let Some(ref mut bv) = state.bind_variables {
bv.next_field();
}
Action::Render
}
KeyCode::BackTab => {
if let Some(ref mut bv) = state.bind_variables {
bv.prev_field();
}
Action::Render
}
KeyCode::Enter => {
if let Some(bv) = state.bind_variables.take() {
state.overlay = None;
crate::ui::app::save_bind_variable_values(&bv.variables);
let final_query = bv.substituted_query();
if bv.new_tab {
return Action::ExecuteQueryNewTab {
tab_id: bv.tab_id,
query: final_query,
start_line: bv.start_line,
};
}
return Action::ExecuteQuery {
tab_id: bv.tab_id,
query: final_query,
start_line: bv.start_line,
};
}
Action::Render
}
KeyCode::Backspace => {
if let Some(ref mut bv) = state.bind_variables {
let idx = bv.selected_idx;
bv.variables[idx].1.pop();
}
Action::Render
}
KeyCode::Char(c) => {
if let Some(ref mut bv) = state.bind_variables {
let idx = bv.selected_idx;
bv.variables[idx].1.push(c);
}
Action::Render
}
_ => Action::None,
}
}
fn query_block_at_cursor(lines: &[String], cursor_row: usize) -> (String, usize) {
let row = cursor_row;
if row >= lines.len() {
return (String::new(), 0);
}
let mut start = row;
let mut blanks = 0;
if start > 0 {
let mut i = row;
while i > 0 {
i -= 1;
if lines[i].trim().is_empty() {
blanks += 1;
if blanks >= 2 {
start = i + blanks; break;
}
} else {
blanks = 0;
start = i;
}
}
if blanks < 2 {
start = if lines[0].trim().is_empty() && blanks >= 1 {
row.saturating_sub(blanks) + 1
} else {
0
};
}
}
let mut end = row;
blanks = 0;
for (i, line) in lines.iter().enumerate().skip(row + 1) {
if line.trim().is_empty() {
blanks += 1;
if blanks >= 2 {
break;
}
} else {
blanks = 0;
end = i;
}
}
while start <= end && lines[start].trim().is_empty() {
start += 1;
}
while end > start && lines[end].trim().is_empty() {
end -= 1;
}
if start > end {
return (String::new(), 0);
}
(lines[start..=end].join("\n"), start)
}
fn persist_group_names(state: &AppState) -> Vec<String> {
state
.tree
.iter()
.filter_map(|n| {
if let TreeNode::Group { name, .. } = n {
return Some(name.clone());
}
None
})
.collect()
}
fn compute_diff_signs(original: &str, current: &[String]) -> HashMap<usize, GutterSign> {
let orig: Vec<&str> = original.lines().collect();
let cur: Vec<&str> = current.iter().map(|s| s.as_str()).collect();
let mut signs = HashMap::new();
if orig.len() == cur.len()
&& orig
.iter()
.zip(cur.iter())
.all(|(a, b)| a.trim_end() == b.trim_end())
{
return signs;
}
let n = orig.len();
let m = cur.len();
let lines_eq = |a: &str, b: &str| -> bool { a.trim_end() == b.trim_end() };
let mut dp = vec![vec![0u32; m + 1]; n + 1];
for i in 1..=n {
for j in 1..=m {
dp[i][j] = if lines_eq(orig[i - 1], cur[j - 1]) {
dp[i - 1][j - 1] + 1
} else {
dp[i - 1][j].max(dp[i][j - 1])
};
}
}
let mut i = n;
let mut j = m;
let mut cur_matched = vec![false; m];
let mut orig_matched = vec![false; n];
while i > 0 && j > 0 {
if lines_eq(orig[i - 1], cur[j - 1]) {
cur_matched[j - 1] = true;
orig_matched[i - 1] = true;
i -= 1;
j -= 1;
} else if dp[i - 1][j] > dp[i][j - 1] {
i -= 1;
} else if dp[i][j - 1] > dp[i - 1][j] {
j -= 1;
} else {
if i > j {
i -= 1;
} else {
j -= 1;
}
}
}
{
let unmatched_cur_count = cur_matched.iter().filter(|&&m| !m).count();
let unmatched_orig_count = orig_matched.iter().filter(|&&m| !m).count();
let need_cur_unmatched = m.saturating_sub(n);
let need_orig_unmatched = n.saturating_sub(m);
if unmatched_cur_count < need_cur_unmatched {
let deficit = need_cur_unmatched - unmatched_cur_count;
let mut candidates: Vec<(usize, usize, usize)> = Vec::new(); let (mut oi, mut ci) = (0, 0);
while oi < n && ci < m {
if orig_matched[oi] && cur_matched[ci] && lines_eq(orig[oi], cur[ci]) {
if cur[ci].trim().is_empty() && oi != ci {
candidates.push((ci, oi, oi.abs_diff(ci)));
}
oi += 1;
ci += 1;
} else if !orig_matched[oi] {
oi += 1;
} else {
ci += 1;
}
}
candidates.sort_by(|a, b| b.2.cmp(&a.2));
for (ci, oi, _) in candidates.into_iter().take(deficit) {
cur_matched[ci] = false;
orig_matched[oi] = false;
}
}
if unmatched_orig_count < need_orig_unmatched {
let deficit = need_orig_unmatched - unmatched_orig_count;
let mut candidates: Vec<(usize, usize, usize)> = Vec::new();
let (mut oi, mut ci) = (0, 0);
while oi < n && ci < m {
if orig_matched[oi] && cur_matched[ci] && lines_eq(orig[oi], cur[ci]) {
if orig[oi].trim().is_empty() && oi != ci {
candidates.push((oi, ci, oi.abs_diff(ci)));
}
oi += 1;
ci += 1;
} else if !orig_matched[oi] {
oi += 1;
} else {
ci += 1;
}
}
candidates.sort_by(|a, b| b.2.cmp(&a.2));
for (oi, ci, _) in candidates.into_iter().take(deficit) {
orig_matched[oi] = false;
cur_matched[ci] = false;
}
}
let unmatched_cur_count = cur_matched.iter().filter(|&&m| !m).count();
let unmatched_orig_count = orig_matched.iter().filter(|&&m| !m).count();
if n == m && unmatched_cur_count == 0 && unmatched_orig_count == 0 {
let mut candidates: Vec<(usize, usize, usize)> = Vec::new();
let (mut oi, mut ci) = (0, 0);
while oi < n && ci < m {
if orig_matched[oi] && cur_matched[ci] && lines_eq(orig[oi], cur[ci]) {
if orig[oi].trim().is_empty() && oi != ci {
candidates.push((oi, ci, oi.abs_diff(ci)));
}
oi += 1;
ci += 1;
} else if !orig_matched[oi] {
oi += 1;
} else {
ci += 1;
}
}
if !candidates.is_empty() {
candidates.sort_by(|a, b| b.2.cmp(&a.2));
let (oi, ci, _) = candidates[0];
orig_matched[oi] = false;
cur_matched[ci] = false;
}
}
}
let unmatched_orig: Vec<usize> = orig_matched
.iter()
.enumerate()
.filter(|(_, m)| !*m)
.map(|(i, _)| i)
.collect();
let unmatched_cur: Vec<usize> = cur_matched
.iter()
.enumerate()
.filter(|(_, m)| !*m)
.map(|(i, _)| i)
.collect();
let mut claimed_cur = vec![false; unmatched_cur.len()];
for &oi in &unmatched_orig {
let orig_line = orig[oi];
let mut best: Option<(usize, usize)> = None; for (k, &ci) in unmatched_cur.iter().enumerate() {
if claimed_cur[k] {
continue;
}
let cur_line = cur[ci];
let common_prefix = orig_line
.chars()
.zip(cur_line.chars())
.take_while(|(a, b)| a == b)
.count();
let common_suffix = orig_line
.chars()
.rev()
.zip(cur_line.chars().rev())
.take_while(|(a, b)| a == b)
.count();
let similarity = common_prefix + common_suffix;
let min_len = orig_line.len().min(cur_line.len()).max(1);
if similarity * 3 > min_len && (best.is_none() || similarity > best.unwrap().1) {
best = Some((k, similarity));
}
}
if let Some((k, _)) = best {
claimed_cur[k] = true;
signs.insert(unmatched_cur[k], GutterSign::Modified);
}
}
for (k, &ci) in unmatched_cur.iter().enumerate() {
if !claimed_cur[k] {
signs.insert(ci, GutterSign::Added);
}
}
let has_unpaired_orig = unmatched_orig.len() > unmatched_cur.len();
if has_unpaired_orig {
let mut orig_to_cur: Vec<Option<usize>> = vec![None; n];
let (mut oi2, mut ci2) = (0, 0);
while oi2 < n && ci2 < m {
if orig_matched[oi2] && cur_matched[ci2] && lines_eq(orig[oi2], cur[ci2]) {
orig_to_cur[oi2] = Some(ci2);
oi2 += 1;
ci2 += 1;
} else if !orig_matched[oi2] {
oi2 += 1;
} else {
ci2 += 1;
}
}
let paired_count = claimed_cur.iter().filter(|&&c| c).count();
let mut paired_orig_count = 0;
for &oi in &unmatched_orig {
if paired_orig_count < paired_count {
paired_orig_count += 1;
continue;
}
let cur_pos = orig_to_cur[oi..].iter().find_map(|c| *c);
if let Some(pos) = cur_pos {
if pos > 0 {
signs.entry(pos - 1).or_insert(GutterSign::DeletedBelow);
} else {
signs.entry(0).or_insert(GutterSign::DeletedAbove);
}
} else if !cur.is_empty() {
signs
.entry(cur.len() - 1)
.or_insert(GutterSign::DeletedBelow);
}
}
}
signs
}