diff --git a/Cargo.lock b/Cargo.lock index 1c04e5e054..dc92110071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6651,7 +6651,7 @@ checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" [[package]] name = "main" -version = "0.7.1" +version = "0.7.2" dependencies = [ "anyhow", "base64 0.22.1", @@ -7755,6 +7755,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "sys-locale", "thiserror 2.0.18", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index ef5f51ffcd..f5883fdb65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ serde_yaml = "0.9" serde_repr = "0.1" smallvec = "1" smol = "2" +sys-locale = "0.3.2" tracing = "0.1.41" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 342c516ae0..f3b20a4f46 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -30,3 +30,4 @@ ed25519-dalek = "2.2.0" parking_lot.workspace = true util.workspace = true rust-i18n.workspace = true +sys-locale.workspace = true diff --git a/crates/core/src/settings.rs b/crates/core/src/settings.rs index e40050cc85..1d94834e98 100644 --- a/crates/core/src/settings.rs +++ b/crates/core/src/settings.rs @@ -10,6 +10,12 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use tracing::{error, info}; +mod locale; + +pub use locale::{ + LOCALE_EN, LOCALE_SYSTEM, LOCALE_ZH_CN, LOCALE_ZH_HK, effective_locale_for_setting, +}; + // ============================================================================ // 全局用户状态 // ============================================================================ @@ -343,7 +349,7 @@ where #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSettings { - #[serde(default)] + #[serde(default = "default_locale")] pub locale: String, #[serde(default)] pub theme_mode: String, @@ -423,6 +429,10 @@ fn default_font_family() -> String { "Arial".to_string() } +fn default_locale() -> String { + LOCALE_SYSTEM.to_string() +} + fn default_font_size() -> f64 { 14.0 } @@ -473,7 +483,7 @@ fn default_sql_query_max_rows() -> u32 { impl Default for AppSettings { fn default() -> Self { Self { - locale: "zh-CN".to_string(), + locale: default_locale(), theme_mode: "light".to_string(), auto_switch_theme: false, font_family: default_font_family(), @@ -607,7 +617,7 @@ impl AppSettings { } pub fn apply(&self, cx: &mut App) { - gpui_component::set_locale(&self.locale); + gpui_component::set_locale(effective_locale_for_setting(&self.locale)); let mode = if self.theme_mode == "dark" { ThemeMode::Dark @@ -638,7 +648,8 @@ impl AppSettings { #[cfg(test)] mod tests { use super::{ - AppSettings, CustomFont, LargeTextCellEditorOpenMode, McpPermissionMode, McpServerMode, + AppSettings, CustomFont, LOCALE_SYSTEM, LargeTextCellEditorOpenMode, McpPermissionMode, + McpServerMode, }; #[test] @@ -678,6 +689,13 @@ mod tests { assert_eq!(1000, settings.sql_query_max_rows); } + #[test] + fn app_settings_default_follows_system_locale() { + let settings = AppSettings::default(); + + assert_eq!(LOCALE_SYSTEM, settings.locale); + } + #[test] fn app_settings_deserializes_sql_query_max_rows_from_legacy_json() { let settings: AppSettings = serde_json::from_value(serde_json::json!({ @@ -689,6 +707,16 @@ mod tests { assert_eq!(1000, settings.sql_query_max_rows); } + #[test] + fn app_settings_deserializes_missing_locale_as_system_mode() { + let settings: AppSettings = serde_json::from_value(serde_json::json!({ + "theme_mode": "dark" + })) + .expect("缺少 locale 的旧版 settings.json 应能读取"); + + assert_eq!(LOCALE_SYSTEM, settings.locale); + } + #[test] fn app_settings_deserializes_mcp_defaults_from_legacy_json() { let settings: AppSettings = serde_json::from_value(serde_json::json!({ diff --git a/crates/core/src/settings/locale.rs b/crates/core/src/settings/locale.rs new file mode 100644 index 0000000000..968408392c --- /dev/null +++ b/crates/core/src/settings/locale.rs @@ -0,0 +1,88 @@ +pub const LOCALE_SYSTEM: &str = "system"; +pub const LOCALE_EN: &str = "en"; +pub const LOCALE_ZH_CN: &str = "zh-CN"; +pub const LOCALE_ZH_HK: &str = "zh-HK"; + +pub fn effective_locale_for_setting(locale_setting: &str) -> &'static str { + resolve_locale_setting(locale_setting, sys_locale::get_locale().as_deref()) +} + +fn resolve_locale_setting(locale_setting: &str, system_locale: Option<&str>) -> &'static str { + match locale_setting { + LOCALE_EN => LOCALE_EN, + LOCALE_ZH_CN => LOCALE_ZH_CN, + LOCALE_ZH_HK => LOCALE_ZH_HK, + LOCALE_SYSTEM | "" => system_locale + .and_then(supported_locale_from_system_locale) + .unwrap_or(LOCALE_EN), + _ => LOCALE_EN, + } +} + +fn supported_locale_from_system_locale(system_locale: &str) -> Option<&'static str> { + let normalized = normalize_system_locale(system_locale); + let mut parts = normalized.split('-'); + let language = parts.next()?; + + match language { + "en" => Some(LOCALE_EN), + "zh" => { + if is_traditional_chinese_locale(&normalized) { + Some(LOCALE_ZH_HK) + } else { + Some(LOCALE_ZH_CN) + } + } + _ => None, + } +} + +fn normalize_system_locale(system_locale: &str) -> String { + system_locale + .split(['.', '@']) + .next() + .unwrap_or_default() + .trim() + .replace('_', "-") + .to_ascii_lowercase() +} + +fn is_traditional_chinese_locale(normalized_locale: &str) -> bool { + normalized_locale + .split('-') + .any(|part| matches!(part, "hant" | "hk" | "tw" | "mo" | "cht" | "traditional")) +} + +#[cfg(test)] +mod tests { + use super::{LOCALE_SYSTEM, resolve_locale_setting, supported_locale_from_system_locale}; + + #[test] + fn supported_locale_from_system_locale_maps_cross_platform_values() { + assert_eq!( + Some("zh-CN"), + supported_locale_from_system_locale("zh-Hans-CN") + ); + assert_eq!( + Some("zh-CN"), + supported_locale_from_system_locale("zh_CN.UTF-8") + ); + assert_eq!( + Some("zh-HK"), + supported_locale_from_system_locale("zh-Hant-HK") + ); + assert_eq!(Some("zh-HK"), supported_locale_from_system_locale("zh_TW")); + assert_eq!(Some("en"), supported_locale_from_system_locale("en-US")); + assert_eq!(None, supported_locale_from_system_locale("fr-FR")); + } + + #[test] + fn resolve_locale_setting_uses_system_fallback_for_system_mode() { + assert_eq!("en", resolve_locale_setting(LOCALE_SYSTEM, None)); + assert_eq!( + "zh-HK", + resolve_locale_setting(LOCALE_SYSTEM, Some("zh-Hant-TW")) + ); + assert_eq!("en", resolve_locale_setting("en", Some("zh-CN"))); + } +} diff --git a/crates/core/src/storage/repository.rs b/crates/core/src/storage/repository.rs index ed2e72ca52..e1d7f9fc4b 100644 --- a/crates/core/src/storage/repository.rs +++ b/crates/core/src/storage/repository.rs @@ -218,7 +218,7 @@ impl Repository for ConnectionRepository { fn list(&self) -> Result> { self.conn.with_connection(|conn| { let mut stmt = conn.prepare( - "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections ORDER BY sort_order ASC, COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", + "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections ORDER BY COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", )?; let rows = stmt.query_map([], |row| ConnectionRow::from_row(row))?; let mut results = Vec::new(); @@ -253,9 +253,9 @@ impl ConnectionRepository { pub fn list_by_workspace(&self, workspace_id: Option) -> Result> { self.conn.with_connection(|conn| { let sql = if workspace_id.is_some() { - "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE workspace_id = ?1 ORDER BY sort_order ASC, COALESCE(last_used_at, updated_at, created_at) DESC, id DESC" + "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE workspace_id = ?1 ORDER BY COALESCE(last_used_at, updated_at, created_at) DESC, id DESC" } else { - "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE workspace_id IS NULL ORDER BY sort_order ASC, COALESCE(last_used_at, updated_at, created_at) DESC, id DESC" + "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE workspace_id IS NULL ORDER BY COALESCE(last_used_at, updated_at, created_at) DESC, id DESC" }; let mut stmt = conn.prepare(sql)?; @@ -321,7 +321,8 @@ impl ConnectionRepository { }) } - /// 批量更新连接的 sort_order(拖拽排序后持久化) + /// 暂停连接拖拽排序:当前连接列表以 LRU 为准,后续重新设计手动排序与 LRU 的关系后再启用。 + #[allow(dead_code)] pub fn update_sort_orders(&self, orders: &[(i64, i32)]) -> Result<()> { self.conn.with_connection(|conn| { for (id, sort_order) in orders { @@ -398,7 +399,7 @@ impl ConnectionRepository { pub fn list_by_team(&self, team_id: &str) -> Result> { self.conn.with_connection(|conn| { let mut stmt = conn.prepare( - "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE team_id = ?1 ORDER BY sort_order ASC, COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", + "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE team_id = ?1 ORDER BY COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", )?; let rows = stmt.query_map(params![team_id], |row| ConnectionRow::from_row(row))?; let mut results = Vec::new(); @@ -413,7 +414,7 @@ impl ConnectionRepository { pub fn list_personal(&self) -> Result> { self.conn.with_connection(|conn| { let mut stmt = conn.prepare( - "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE team_id IS NULL ORDER BY sort_order ASC, COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", + "SELECT id, name, connection_type, params, workspace_id, selected_databases, remark, sync_enabled, cloud_id, last_synced_at, last_used_at, sort_order, created_at, updated_at, team_id, owner_id FROM connections WHERE team_id IS NULL ORDER BY COALESCE(last_used_at, updated_at, created_at) DESC, id DESC", )?; let rows = stmt.query_map([], |row| ConnectionRow::from_row(row))?; let mut results = Vec::new(); @@ -868,6 +869,33 @@ mod tests { assert_eq!(1000, updated_at); assert!(last_used_at.is_some()); } + + #[test] + fn list_ignores_legacy_sort_order_for_recent_use() { + let (conn, repo) = test_repository(); + let mut old_connection = ssh_connection("old"); + let old_id = repo.insert(&mut old_connection).unwrap(); + let mut new_connection = ssh_connection("new"); + let new_id = repo.insert(&mut new_connection).unwrap(); + + conn.with_connection(|conn| { + conn.execute( + "UPDATE connections SET created_at = ?1, updated_at = ?1, sort_order = ?2 WHERE id = ?3", + params![1000i64, 0i32, old_id], + )?; + conn.execute( + "UPDATE connections SET created_at = ?1, updated_at = ?1, sort_order = ?2 WHERE id = ?3", + params![2000i64, 100i32, new_id], + )?; + Ok(()) + }) + .unwrap(); + + assert_eq!( + Some(new_id), + repo.list().unwrap().first().and_then(|c| c.id) + ); + } } pub fn init(cx: &mut App) { diff --git a/crates/db/src/manager.rs b/crates/db/src/manager.rs index 20342ce42e..a4c023343b 100644 --- a/crates/db/src/manager.rs +++ b/crates/db/src/manager.rs @@ -1008,13 +1008,32 @@ impl GlobalDbState { database: String, table_name: String, ) -> anyhow::Result { - let config = self + self.truncate_table_with_schema(cx, config_id, database, None, table_name) + .await + } + + /// Truncate table with an optional schema. + pub async fn truncate_table_with_schema( + &self, + cx: &mut AsyncApp, + config_id: String, + database: String, + schema: Option, + table_name: String, + ) -> anyhow::Result { + let mut config = self .get_config(&config_id) .ok_or_else(|| anyhow::anyhow!("Connection not found: {}", config_id))?; let plugin = self.get_plugin(&config.database_type)?; - let sql = plugin.truncate_table(&database, &table_name); + let sql = plugin.truncate_table_with_schema(&database, schema.as_deref(), &table_name); - let result = self.execute_with_session(cx, config, sql, None).await?; + if config.database_type != DatabaseType::Oracle { + config.database = Some(database); + } + + let result = self + .execute_with_session_internal(cx, config, sql, None, schema) + .await?; Self::wrapper_result(result) } diff --git a/crates/db/src/plugin.rs b/crates/db/src/plugin.rs index 780db41d1c..ce8b46945f 100644 --- a/crates/db/src/plugin.rs +++ b/crates/db/src/plugin.rs @@ -2325,6 +2325,16 @@ pub trait DatabasePlugin: Send + Sync { format!("TRUNCATE TABLE {}", self.quote_identifier(table)) } + /// Truncate table with an optional schema. + fn truncate_table_with_schema( + &self, + database: &str, + _schema: Option<&str>, + table: &str, + ) -> String { + self.truncate_table(database, table) + } + /// Rename table fn rename_table(&self, database: &str, old_name: &str, new_name: &str) -> String; diff --git a/crates/db/src/postgresql/plugin.rs b/crates/db/src/postgresql/plugin.rs index 0b78815a10..a02a0e4b95 100644 --- a/crates/db/src/postgresql/plugin.rs +++ b/crates/db/src/postgresql/plugin.rs @@ -1918,6 +1918,22 @@ impl DatabasePlugin for PostgresPlugin { } } + fn truncate_table_with_schema( + &self, + _database: &str, + schema: Option<&str>, + table: &str, + ) -> String { + if let Some(schema) = schema { + return format!( + "TRUNCATE TABLE {}.{}", + self.quote_identifier(schema), + self.quote_identifier(table) + ); + } + format!("TRUNCATE TABLE {}", self.quote_identifier(table)) + } + fn rename_table(&self, _database: &str, old_name: &str, new_name: &str) -> String { format!( "ALTER TABLE {} RENAME TO {}", @@ -2307,6 +2323,14 @@ mod tests { assert!(sql.contains("\"users\"")); } + #[test] + fn test_truncate_table_with_schema() { + let plugin = create_plugin(); + let sql = plugin.truncate_table_with_schema("test_db", Some("app"), "users"); + assert_eq!(sql, "TRUNCATE TABLE \"app\".\"users\""); + assert!(!sql.contains("test_db")); + } + #[test] fn test_rename_table() { let plugin = create_plugin(); diff --git a/crates/db_view/src/db_tree_event.rs b/crates/db_view/src/db_tree_event.rs index dba0c476ea..07f834eaf2 100644 --- a/crates/db_view/src/db_tree_event.rs +++ b/crates/db_view/src/db_tree_event.rs @@ -3237,8 +3237,15 @@ impl DatabaseEventHandler { .get("database") .map(|s| s.to_string()) .unwrap_or_default(); + let schema = meta.get("schema").map(|s| s.to_string()); let task = state - .truncate_table(cx, conn_id.clone(), database, tbl_name.clone()) + .truncate_table_with_schema( + cx, + conn_id.clone(), + database, + schema, + tbl_name.clone(), + ) .await; match task { diff --git a/crates/db_view/src/db_tree_view.rs b/crates/db_view/src/db_tree_view.rs index 6aa9aaf978..00476e2d91 100644 --- a/crates/db_view/src/db_tree_view.rs +++ b/crates/db_view/src/db_tree_view.rs @@ -3025,6 +3025,7 @@ mod tests { last_used_at: None, created_at: None, updated_at: None, + sort_order: None, team_id: None, owner_id: None, } diff --git a/crates/terminal_view/src/theme.rs b/crates/terminal_view/src/theme.rs index 76a6949fea..ab32d63d07 100644 --- a/crates/terminal_view/src/theme.rs +++ b/crates/terminal_view/src/theme.rs @@ -95,6 +95,8 @@ pub fn default_font_fallbacks() -> Vec { "Courier New".into(), "Apple Color Emoji".into(), "Apple Symbols".into(), + "Noto Sans Mono CJK SC".into(), + "Source Han Mono SC".into(), "PingFang SC".into(), "PingFang TC".into(), "Hiragino Sans GB".into(), @@ -105,6 +107,8 @@ pub fn default_font_fallbacks() -> Vec { "Courier New".into(), "Lucida Console".into(), "Segoe UI Emoji".into(), + "Noto Sans Mono CJK SC".into(), + "Source Han Mono SC".into(), "Microsoft YaHei".into(), "SimSun".into(), ] @@ -115,6 +119,8 @@ pub fn default_font_fallbacks() -> Vec { "Liberation Mono".into(), "Courier New".into(), "Noto Color Emoji".into(), + "Noto Sans Mono CJK SC".into(), + "Source Han Mono SC".into(), "Noto Sans CJK SC".into(), "WenQuanYi Micro Hei".into(), ] @@ -426,3 +432,32 @@ impl TerminalTheme { } } } + +#[cfg(test)] +mod tests { + use super::default_font_fallbacks; + + #[test] + fn terminal_default_fallbacks_include_monospace_cjk_fonts_first() { + let fallbacks = default_font_fallbacks() + .into_iter() + .map(|font| font.to_string()) + .collect::>(); + + let noto_mono = fallbacks + .iter() + .position(|font| font == "Noto Sans Mono CJK SC") + .expect("Noto Sans Mono CJK SC should be a terminal fallback"); + let source_han_mono = fallbacks + .iter() + .position(|font| font == "Source Han Mono SC") + .expect("Source Han Mono SC should be a terminal fallback"); + + for ui_font in ["PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"] { + if let Some(ui_index) = fallbacks.iter().position(|font| font == ui_font) { + assert!(noto_mono < ui_index); + assert!(source_han_mono < ui_index); + } + } + } +} diff --git a/crates/terminal_view/src/view.rs b/crates/terminal_view/src/view.rs index 6bd93a4d55..bcf820f908 100644 --- a/crates/terminal_view/src/view.rs +++ b/crates/terminal_view/src/view.rs @@ -181,6 +181,14 @@ fn should_start_selection_from_pending_sgr_press(start: AlacPoint, current: Alac start != current } +fn should_extend_selection_on_shift_click( + button: MouseButton, + modifiers: Modifiers, + has_selection: bool, +) -> bool { + button == MouseButton::Left && modifiers.shift && has_selection +} + fn should_scroll_to_bottom_on_user_input( display_offset: usize, pending_display_offset: &StdCell>, @@ -3650,6 +3658,17 @@ impl TerminalView { let bounds = self.terminal_bounds; let point = self.pixel_to_point(event.position, bounds, cx); + let has_selection = self.terminal.read(cx).term().lock().selection.is_some(); + if should_extend_selection_on_shift_click(event.button, event.modifiers, has_selection) { + let side = self.pixel_to_side(event.position, bounds); + self.terminal.update(cx, |terminal, _| { + terminal.update_selection(point, side); + }); + self.mouse_state.selecting = true; + cx.notify(); + return; + } + let screen_line = point.line.0 as usize; let column = point.column.0; let line_text = self.get_line_text(screen_line, cx); @@ -3744,39 +3763,16 @@ impl TerminalView { let point = self.pixel_to_point(event.position, bounds, cx); let screen_line = point.line.0 as usize; let column = point.column.0; - if let Some(pending) = &self.mouse_state.pending_sgr_left_press { - if should_start_selection_from_pending_sgr_press(pending.point, point) { - let pending = self.mouse_state.pending_sgr_left_press.take().unwrap(); - let now = std::time::Instant::now(); - let is_double_click = self.mouse_state.last_click_point == Some(pending.point) - && self - .mouse_state - .last_click_time - .map_or(false, |t| now.duration_since(t).as_millis() < 500); - - self.mouse_state.click_count = if is_double_click { - self.mouse_state.click_count + 1 - } else { - 1 - }; - self.mouse_state.last_click_point = Some(pending.point); - self.mouse_state.last_click_time = Some(now); - let selection_type = match self.mouse_state.click_count { - 1 => SelectionType::Simple, - 2 => SelectionType::Semantic, - _ => SelectionType::Lines, - }; - self.terminal.update(cx, |terminal, _| { - terminal.start_selection( - selection_type, - pending.point, - self.pixel_to_side(pending.position, bounds), - ); - }); - self.mouse_state.selecting = true; - } + if !event.dragging() { + self.mouse_state.pending_sgr_left_press = None; + self.finish_mouse_selection(cx); } + + if event.dragging() { + self.start_selection_from_pending_sgr_press(point, bounds, cx); + } + let line_text = self.get_line_text(screen_line, cx); let is_local = self.terminal.read(cx).connection_kind() == TerminalConnectionKind::Local; let hover_changed = { @@ -3801,6 +3797,11 @@ impl TerminalView { return; } + if !event.dragging() { + self.finish_mouse_selection(cx); + return; + } + let point = self.pixel_to_point(event.position, bounds, cx); let side = self.pixel_to_side(event.position, bounds); @@ -3810,6 +3811,54 @@ impl TerminalView { cx.notify(); } + fn start_selection_from_pending_sgr_press( + &mut self, + point: AlacPoint, + bounds: Bounds, + cx: &mut Context, + ) { + let should_start = self + .mouse_state + .pending_sgr_left_press + .as_ref() + .map_or(false, |pending| { + should_start_selection_from_pending_sgr_press(pending.point, point) + }); + if !should_start { + return; + } + + let pending = self.mouse_state.pending_sgr_left_press.take().unwrap(); + let now = std::time::Instant::now(); + let is_double_click = self.mouse_state.last_click_point == Some(pending.point) + && self + .mouse_state + .last_click_time + .map_or(false, |t| now.duration_since(t).as_millis() < 500); + + self.mouse_state.click_count = if is_double_click { + self.mouse_state.click_count + 1 + } else { + 1 + }; + self.mouse_state.last_click_point = Some(pending.point); + self.mouse_state.last_click_time = Some(now); + let selection_type = match self.mouse_state.click_count { + 1 => SelectionType::Simple, + 2 => SelectionType::Semantic, + _ => SelectionType::Lines, + }; + + self.terminal.update(cx, |terminal, _| { + terminal.start_selection( + selection_type, + pending.point, + self.pixel_to_side(pending.position, bounds), + ); + }); + self.mouse_state.selecting = true; + } + fn handle_mouse_up( &mut self, event: &MouseUpEvent, @@ -3871,6 +3920,34 @@ impl TerminalView { ); let _ = self.addon_manager.dispatch_mouse_up(&mut context); } + self.finish_mouse_selection(cx); + } + + fn handle_window_mouse_up( + &mut self, + event: &MouseUpEvent, + window: &mut Window, + cx: &mut Context, + ) { + if event.button != MouseButton::Left { + return; + } + + if self.mouse_state.pending_sgr_left_press.is_some() { + if !self.terminal_bounds.contains(&event.position) { + self.handle_mouse_up(event, window, cx); + } + return; + } + + self.finish_mouse_selection(cx); + } + + fn finish_mouse_selection(&mut self, cx: &mut Context) { + if !self.mouse_state.selecting { + return; + } + self.mouse_state.selecting = false; if self.auto_copy_on_select { if let Some(text) = self.terminal.read(cx).selection_text() { @@ -4453,6 +4530,15 @@ impl Element for ResizeEventHandler { } } }); + + window.on_mouse_event({ + let view = self.view.clone(); + move |e: &MouseUpEvent, phase, window, cx| { + if phase.bubble() { + view.update(cx, |view, cx| view.handle_window_mouse_up(e, window, cx)); + } + } + }); } } @@ -4467,8 +4553,8 @@ mod tests { should_defer_inline_history_prompt_input_to_text_system, should_defer_sgr_left_press, should_dismiss_history_prompt_for_keystroke, should_dismiss_history_prompt_for_mouse, should_dismiss_history_prompt_for_scroll, should_reset_history_prompt_for_terminal_event, - should_scroll_to_bottom_on_user_input, should_start_selection_from_pending_sgr_press, - take_whole_scroll_lines, + should_extend_selection_on_shift_click, should_scroll_to_bottom_on_user_input, + should_start_selection_from_pending_sgr_press, take_whole_scroll_lines, }; use crate::history_prompt::{HistoryPromptAccept, HistoryPromptState}; use alacritty_terminal::index::{Column, Line, Point as AlacPoint}; @@ -4565,6 +4651,14 @@ mod tests { assert!(source.contains("this.clear_screen(&ClearScreen, window, cx)")); } + #[test] + fn terminal_selection_has_window_mouse_up_fallback() { + let source = include_str!("view.rs"); + + assert!(source.matches("handle_window_mouse_up").count() >= 2); + assert!(source.contains("window.on_mouse_event({")); + } + #[test] fn terminal_reset_font_size_is_fifteen() { assert_eq!(super::TERMINAL_RESET_FONT_SIZE, 15.0); @@ -4730,6 +4824,36 @@ mod tests { )); } + #[test] + fn shift_left_click_extends_existing_terminal_selection_only() { + let shift = Modifiers { + shift: true, + ..Default::default() + }; + let none = Modifiers::default(); + + assert!(should_extend_selection_on_shift_click( + MouseButton::Left, + shift, + true + )); + assert!(!should_extend_selection_on_shift_click( + MouseButton::Left, + shift, + false + )); + assert!(!should_extend_selection_on_shift_click( + MouseButton::Left, + none, + true + )); + assert!(!should_extend_selection_on_shift_click( + MouseButton::Right, + shift, + true + )); + } + #[test] fn multiline_non_empty_line_count_ignores_blank_lines() { assert_eq!(multiline_non_empty_line_count("echo 1\n\n echo 2\n"), 2); diff --git a/main/Cargo.toml b/main/Cargo.toml index 1343116fa2..e2b59c5baf 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "main" -version = "0.7.1" +version = "0.7.2" publish.workspace = true edition.workspace = true diff --git a/main/locales/main.yml b/main/locales/main.yml index b46652e732..bd537aad81 100644 --- a/main/locales/main.yml +++ b/main/locales/main.yml @@ -692,6 +692,10 @@ Settings: en: Switch the interface display language, some content requires restarting the application to take effect zh-CN: 切换界面显示语言,部分内容需要重启应用后生效 zh-HK: 切換介面顯示語言,部分內容需要重啟應用後生效 + system: + en: Follow System + zh-CN: 跟随系统 + zh-HK: 跟隨系統 zh_cn: en: Simplified Chinese zh-CN: 简体中文 diff --git a/main/src/home_tab.rs b/main/src/home_tab.rs index 9db18b74a3..94a4e9ec85 100644 --- a/main/src/home_tab.rs +++ b/main/src/home_tab.rs @@ -153,6 +153,7 @@ impl ConnectionLayout { /// 拖拽排序的荷载,在渲染连接列表时由 on_drag 创建, /// 拖入另一个连接条目时由 on_drop handler 读取。 #[derive(Clone)] +#[allow(dead_code)] struct DragConnection { source_index: usize, source_id: Option, @@ -569,6 +570,7 @@ impl HomePage { } /// 拖拽排序:将 `from` 位置的连接移动到 `to` 位置,并异步持久化 sort_order。 + #[allow(dead_code)] fn reorder_connections(&mut self, from: usize, to: usize, cx: &mut Context) { if from >= self.connections.len() || to >= self.connections.len() || from == to { return; @@ -593,6 +595,7 @@ impl HomePage { cx.notify(); } + #[allow(dead_code)] fn reorder_connection_by_id( &mut self, source_id: Option, @@ -3104,7 +3107,7 @@ impl HomePage { &self, conn: StoredConnection, selected_id: Option, - index: usize, + _index: usize, cx: &mut Context, ) -> AnyElement { let conn_id = conn.id; @@ -3122,10 +3125,6 @@ impl HomePage { .map_or(false, |id| cx.global::().is_active(id)); let can_edit = can_edit_connection(&conn, cx); - let accent = cx.theme().accent; - let drag_connection = self.drag_connection(&conn, index); - let drop_target_id = conn.id; - h_flex() .id(SharedString::from(format!( "conn-list-item-{}", @@ -3156,21 +3155,6 @@ impl HomePage { this.selected_connection_id = conn_id; cx.notify(); })) - .on_drag(drag_connection, |drag, _, window, cx| { - window.prevent_default(); - cx.stop_propagation(); - cx.new(|_| drag.clone()) - }) - .drag_over::(move |el, _, _, _cx| el.border_t_2().border_color(accent)) - .on_drop( - cx.listener(move |this, drag: &DragConnection, _window, cx| { - if drag.source_id.is_some() && drop_target_id.is_some() { - this.reorder_connection_by_id(drag.source_id, drop_target_id, cx); - } else if drag.source_index != index { - this.reorder_connections(drag.source_index, index, cx); - } - }), - ) // 激活指示灯 .when(is_active, |this| { // list_item version @@ -3492,6 +3476,7 @@ impl HomePage { } } + #[allow(dead_code)] fn drag_connection(&self, conn: &StoredConnection, index: usize) -> DragConnection { DragConnection { source_index: index, @@ -3506,7 +3491,7 @@ impl HomePage { &self, conn: StoredConnection, selected_id: Option, - index: usize, + _index: usize, cx: &mut Context, ) -> AnyElement { let conn_id = conn.id; @@ -3526,10 +3511,6 @@ impl HomePage { let can_edit = can_edit_connection(&conn, cx); let has_team = conn.team_id.is_some(); - let accent = cx.theme().accent; - let drag_connection = self.drag_connection(&conn, index); - let drop_target_id = conn.id; - let card = v_flex() .justify_center() .id(SharedString::from(format!( @@ -3567,21 +3548,6 @@ impl HomePage { this.selected_connection_id = conn_id; cx.notify(); })) - .on_drag(drag_connection, |drag, _, window, cx| { - window.prevent_default(); - cx.stop_propagation(); - cx.new(|_| drag.clone()) - }) - .drag_over::(move |el, _, _, _cx| el.border_l_2().border_color(accent)) - .on_drop( - cx.listener(move |this, drag: &DragConnection, _window, cx| { - if drag.source_id.is_some() && drop_target_id.is_some() { - this.reorder_connection_by_id(drag.source_id, drop_target_id, cx); - } else if drag.source_index != index { - this.reorder_connections(drag.source_index, index, cx); - } - }), - ) .when(is_active, |this| { // card version this.child( diff --git a/main/src/setting_tab.rs b/main/src/setting_tab.rs index 41914ccb99..f2bf5dd3ab 100644 --- a/main/src/setting_tab.rs +++ b/main/src/setting_tab.rs @@ -39,8 +39,9 @@ pub const DEFAULT_SYSTEM_HOTKEY_MACOS: &str = "cmd-alt-m"; pub const DEFAULT_SYSTEM_HOTKEY_OTHER: &str = "ctrl-alt-m"; pub use one_core::settings::{ - AppSettings, CustomFont, DatabaseOpenMode, GlobalCurrentUser, GlobalProxySettings, - LargeTextCellEditorOpenMode, ProxyType, + AppSettings, CustomFont, DatabaseOpenMode, GlobalCurrentUser, GlobalProxySettings, LOCALE_EN, + LOCALE_SYSTEM, LOCALE_ZH_CN, LOCALE_ZH_HK, LargeTextCellEditorOpenMode, ProxyType, + effective_locale_for_setting, }; use one_core::tab_container::{TabContent, TabContentEvent}; use one_core::utils::auto_save_config::AutoSaveConfig; @@ -340,21 +341,27 @@ impl SettingsPanel { SettingField::dropdown( vec![ ( - "zh-CN".into(), + LOCALE_SYSTEM.into(), + t!("Settings.General.Language.system").into(), + ), + ( + LOCALE_ZH_CN.into(), t!("Settings.General.Language.zh_cn").into(), ), ( - "zh-HK".into(), + LOCALE_ZH_HK.into(), t!("Settings.General.Language.zh_hk").into(), ), - ("en".into(), t!("Settings.General.Language.en").into()), + (LOCALE_EN.into(), t!("Settings.General.Language.en").into()), ], |cx: &App| { SharedString::from(AppSettings::global(cx).locale.clone()) }, |val: SharedString, cx: &mut App| { let locale = val.to_string(); - gpui_component::set_locale(&locale); + gpui_component::set_locale(effective_locale_for_setting( + &locale, + )); AppSettings::update_and_save(cx, |settings| { settings.locale = locale; }); diff --git a/script/release-tag.sh b/script/release-tag.sh index fd1856f0dc..2c05ef727f 100755 --- a/script/release-tag.sh +++ b/script/release-tag.sh @@ -5,14 +5,19 @@ set -euo pipefail # 1) 修改 TAG 变量后执行:script/release-tag.sh # 2) 直接传参覆盖 TAG:script/release-tag.sh v0.1.0 # 3) 若需要覆盖同名 tag:FORCE_RETAG=true script/release-tag.sh v0.1.0 -# 4) 脚本会自动同步 main/Cargo.toml 版本并提交后再推送分支和 tag +# 4) 脚本会自动同步 main/Cargo.toml 与 Cargo.lock 版本并提交后再推送分支和 tag + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)" +cd "${REPO_ROOT}" TAG="${1:-v0.1.0}" REMOTE="${REMOTE:-origin}" BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" FORCE_RETAG="${FORCE_RETAG:-false}" ALLOW_DIRTY="${ALLOW_DIRTY:-false}" -MAIN_MANIFEST="${MAIN_MANIFEST:-../main/Cargo.toml}" +MAIN_MANIFEST="${MAIN_MANIFEST:-${REPO_ROOT}/main/Cargo.toml}" +CARGO_LOCK="${CARGO_LOCK:-${REPO_ROOT}/Cargo.lock}" RELEASE_VERSION="${TAG#v}" update_main_version() { @@ -71,6 +76,18 @@ update_main_version() { return 0 } +sync_main_lock_version() { + local new_version="$1" + + if [[ ! -f "${CARGO_LOCK}" ]]; then + echo "错误:未找到 Cargo.lock:${CARGO_LOCK}" + exit 1 + fi + + echo "同步 Cargo.lock 中 main 版本到 ${new_version}" + cargo update --manifest-path "${REPO_ROOT}/Cargo.toml" -p main --precise "${new_version}" +} + echo "准备发布:tag=${TAG} branch=${BRANCH} remote=${REMOTE}" if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then @@ -111,10 +128,15 @@ if [[ "${REMOTE_TAG_EXISTS}" == "true" ]]; then fi fi -if update_main_version "${MAIN_MANIFEST}" "${RELEASE_VERSION}"; then +update_main_version "${MAIN_MANIFEST}" "${RELEASE_VERSION}" || true +sync_main_lock_version "${RELEASE_VERSION}" + +if ! git diff --quiet -- "${MAIN_MANIFEST}" "${CARGO_LOCK}"; then echo "提交 main 版本变更" - git add "${MAIN_MANIFEST}" + git add "${MAIN_MANIFEST}" "${CARGO_LOCK}" git commit -m "chore(main): bump version to ${RELEASE_VERSION}" +else + echo "main 版本和 Cargo.lock 已是 ${RELEASE_VERSION},跳过提交。" fi echo "推送分支:${BRANCH}"