Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 129 additions & 3 deletions crates/db_view/src/sql_editor_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use gpui_component::input::{
};
use gpui_component::notification::Notification;
use gpui_component::select::{SearchableVec, Select, SelectEvent, SelectItem, SelectState};
use gpui_component::spinner::Spinner;
use gpui_component::{
ActiveTheme, Disableable, Icon, IndexPath, Sizable, Size, WindowExt, h_flex, v_flex,
};
Expand Down Expand Up @@ -1242,6 +1243,14 @@ fn query_toolbar_action(is_executing: bool, has_selection: bool) -> QueryToolbar
}
}

/// 结果面板是否渲染。
///
/// 执行中必须渲染:面板里才有加载指示与实时运行时间,否则大查询期间界面上
/// 没有任何反馈,会被误认为执行没有生效。其余情况沿用“有结果且用户未隐藏”。
fn result_panel_visible(has_results: bool, is_visible: bool, is_executing: bool) -> bool {
is_executing || (has_results && is_visible)
}

/// 提交 / 回滚按钮的 loading 只属于正在收尾的那个动作:另一个按钮保持
/// 普通图标(但已被禁用),避免两个按钮同时转圈分不清在提交还是回滚。
fn transaction_action_loading(
Expand Down Expand Up @@ -3556,6 +3565,10 @@ impl SqlEditorTab {
}
let generation = self.auto_session_generation.fetch_add(1, Ordering::SeqCst) + 1;
self.auto_session_starting = true;
// 建会话阶段也计入运行中:首次连接可能耗时数秒。
let pending_run = self
.sql_result_tab_container
.update(cx, |container, cx| container.begin_run_pending(cx));
cx.notify();

let global_state = cx.global::<GlobalDbState>().clone();
Expand All @@ -3573,6 +3586,9 @@ impl SqlEditorTab {
Err(error) => {
let is_current = entity
.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if transaction_generation.load(Ordering::SeqCst) != generation {
return false;
}
Expand All @@ -3597,6 +3613,9 @@ impl SqlEditorTab {
|| context_generation_guard.load(Ordering::SeqCst) != context_generation
{
let _ = entity.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if transaction_generation.load(Ordering::SeqCst) == generation
&& this.auto_session_starting
{
Expand Down Expand Up @@ -3647,6 +3666,9 @@ impl SqlEditorTab {
.unwrap_or(false);
if !installed {
let _ = entity.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if transaction_generation.load(Ordering::SeqCst) == generation
&& this.auto_session_starting
{
Expand Down Expand Up @@ -3769,6 +3791,10 @@ impl SqlEditorTab {
let context_generation_guard = self.context_generation.clone();
self.manual_transaction_starting = true;
self.manual_transaction_finishing = None;
// 建事务会话阶段也计入运行中:首次连接可能耗时数秒。
let pending_run = self
.sql_result_tab_container
.update(cx, |container, cx| container.begin_run_pending(cx));
cx.notify();

let global_state = cx.global::<GlobalDbState>().clone();
Expand All @@ -3784,6 +3810,9 @@ impl SqlEditorTab {
Err(error) => {
let is_current = entity
.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if transaction_generation.load(Ordering::SeqCst) != generation {
return false;
}
Expand All @@ -3808,6 +3837,9 @@ impl SqlEditorTab {
|| context_generation_guard.load(Ordering::SeqCst) != context_generation
{
let _ = entity.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if is_current_manual_transaction_start(
generation,
transaction_generation.load(Ordering::SeqCst),
Expand Down Expand Up @@ -3843,6 +3875,9 @@ impl SqlEditorTab {
}
let is_current = entity
.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if !is_current_manual_transaction_start(
generation,
transaction_generation.load(Ordering::SeqCst),
Expand Down Expand Up @@ -3899,6 +3934,9 @@ impl SqlEditorTab {
Self::notify_async(cx, t!("Query.transaction_started").to_string());
} else {
let _ = entity.update(cx, |this, cx| {
this.sql_result_tab_container.update(cx, |container, cx| {
container.end_run_pending(pending_run, cx);
});
if is_current_manual_transaction_start(
generation,
transaction_generation.load(Ordering::SeqCst),
Expand Down Expand Up @@ -4151,6 +4189,14 @@ impl SqlEditorTab {
let cancelled = self
.sql_result_tab_container
.update(cx, |container, cx| container.cancel_execution(cx));
// Auto 粘性会话的建立阶段还没有已安装会话,取消键必须显式作废这次建立,
// 否则被停下的查询仍会在会话就绪后照常执行。
if cancelled && self.auto_session_starting {
self.auto_session_generation.fetch_add(1, Ordering::SeqCst);
self.auto_session_starting = false;
self.finalize_execution_marker(SqlGutterMarkerState::Cancelled, cx);
cx.notify();
}
match manual_transaction_stop_action(
cancelled,
self.manual_transaction_starting,
Expand Down Expand Up @@ -4740,6 +4786,8 @@ impl SqlEditorTab {
// Check if there are any results and if the panel is visible
let has_results = self.sql_result_tab_container.read(cx).has_results(cx);
let results_visible = self.sql_result_tab_container.read(cx).is_visible(cx);
let is_query_executing = self.sql_result_tab_container.read(cx).is_executing(cx);
let results_hidden = !result_panel_visible(has_results, results_visible, is_query_executing);

v_flex()
.size_full()
Expand All @@ -4756,7 +4804,7 @@ impl SqlEditorTab {
.key_context(SQL_EDITOR_CONTEXT)
.child(editor.clone()),
)
.when(has_results && !results_visible, |this| {
.when(has_results && results_hidden, |this| {
this.child(
h_flex().w_full().justify_end().child(
Button::new("show-results")
Expand Down Expand Up @@ -4847,6 +4895,26 @@ impl SqlEditorTab {
cx,
),
})
// 运行中在工具栏上同步给出反馈:查询数据量大时面板可能被挤得很窄,
// 仅靠面板内的加载态不足以说明“执行已提交”。这里刻意不读
// `total_elapsed_ms`,避免编辑器(含 SQL 输入)被计时器每 100ms
// 重绘一次;实时耗时由结果面板内的“运行时间”滚动展示。
.when(toolbar_action == QueryToolbarAction::Stop, |toolbar| {
toolbar.child(
h_flex()
.gap_1()
.items_center()
.px_1()
.flex_shrink_0()
.child(Spinner::new().with_size(Size::XSmall))
.child(
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(t!("Query.running").to_string()),
),
)
})
.child(Self::query_toolbar_button(
QueryToolbarButtonSpec {
id: "explain-sql",
Expand Down Expand Up @@ -5059,6 +5127,7 @@ impl Render for SqlEditorTab {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_results = self.sql_result_tab_container.read(cx).has_results(cx);
let results_visible = self.sql_result_tab_container.read(cx).is_visible(cx);
let is_query_executing = self.sql_result_tab_container.read(cx).is_executing(cx);
let view = cx.entity().clone();

let mut div = v_flex()
Expand All @@ -5068,7 +5137,7 @@ impl Render for SqlEditorTab {
.on_action(cx.listener(Self::handle_run_selected_sql_action))
.on_action(cx.listener(Self::handle_run_cursor_statement_sql_action))
.on_action(cx.listener(Self::handle_toggle_line_comment_action));
if has_results && results_visible {
if result_panel_visible(has_results, results_visible, is_query_executing) {
div = div
.child(self.render_has_results(window, cx))
.child(ResizeEventHandler { view });
Expand Down Expand Up @@ -5311,7 +5380,8 @@ mod tests {
manual_transaction_invalidation_mode, manual_transaction_stop_action,
match_sql_to_statement_marker, metadata_scope_selection, preferred_default_database,
query_connection_context_label, query_connection_ids, query_file_path_for_name,
query_toolbar_action, schema_changed_event_matches_scope, should_render_schema_select,
query_toolbar_action, result_panel_visible, schema_changed_event_matches_scope,
should_render_schema_select,
sql_text_for_run_all, sql_text_for_toolbar_run, statement_for_gutter_marker,
statement_marker_id, supports_manual_transactions, toggle_line_comment_defaults,
toggle_sql_line_comments, transaction_action_loading, transaction_liveness,
Expand Down Expand Up @@ -5743,6 +5813,62 @@ mod tests {
assert!(!can_start_query_execution(true));
}

#[test]
fn executing_query_keeps_the_result_panel_visible() {
// 执行中即使还没有结果、用户此前关过面板,也必须渲染面板,
// 否则大查询期间界面上没有任何执行指示。
assert!(result_panel_visible(false, false, true));
assert!(result_panel_visible(true, true, true));
// 非执行态沿用“有结果且用户未隐藏”。
assert!(result_panel_visible(true, true, false));
assert!(!result_panel_visible(false, true, false));
assert!(!result_panel_visible(true, false, false));
}

/// 建会话阶段的运行指示必须有始有终:发起处启动,所有中断分支收尾,
/// 否则指示器会永远卡在“运行中”。
#[test]
fn session_start_run_feedback_is_started_and_always_ended() {
// 只看生产代码:本测试自身的字符串字面量也会被 matches 统计到。
let source = include_str!("sql_editor_view.rs");
let production = source.split("#[cfg(test)]\nmod tests {").next().unwrap();

assert_eq!(
2,
production.matches("container.begin_run_pending(cx)").count(),
"Auto 与 Manual 两条建会话路径都要进入运行态"
);

for entry in [
"fn start_auto_session_and_run",
"fn start_manual_transaction_and_run",
] {
let body = production
.split(entry)
.nth(1)
.unwrap()
.split("\n fn ")
.next()
.unwrap();
assert!(
body.contains("container.begin_run_pending(cx)"),
"{entry} 必须在建会话前启动运行指示"
);
assert!(
body.contains("container.end_run_pending(pending_run, cx)"),
"{entry} 的中断分支必须收尾运行指示"
);
}

assert_eq!(
7,
production
.matches("container.end_run_pending(pending_run, cx)")
.count(),
"建会话失败/过期/未安装共 7 个分支都要收尾运行指示"
);
}

#[test]
fn manual_transaction_stop_assigns_exactly_one_session_close_owner() {
assert_eq!(
Expand Down
Loading
Loading