diff --git a/Cargo.lock b/Cargo.lock index 868dab6595..2bd7acadd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3133,7 +3133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -3253,6 +3253,7 @@ dependencies = [ [[package]] name = "ferrum-flow" version = "0.2.1" +source = "git+https://github.com/tu6ge/ferrum-flow.git?rev=507cab7505b8dfa6a1e4a121e3c503e3f2f882d6#507cab7505b8dfa6a1e4a121e3c503e3f2f882d6" dependencies = [ "anyhow", "futures", @@ -5681,7 +5682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.3", ] [[package]] @@ -6048,7 +6049,7 @@ checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" [[package]] name = "main" -version = "0.3.3" +version = "0.4.1" dependencies = [ "anyhow", "base64 0.22.1", @@ -6079,6 +6080,7 @@ dependencies = [ "terminal_view", "tokio", "tracing", + "tracing-appender", "tracing-subscriber", "winresource", "zip 2.4.2", @@ -8245,7 +8247,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9218,7 +9220,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9231,7 +9233,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -9343,7 +9345,7 @@ dependencies = [ "security-framework 3.6.0", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9817,13 +9819,13 @@ dependencies = [ [[package]] name = "serial2" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdbc46aa3882ec3d48ec2b5abcb4f0d863a13d7599265f3faa6d851f23c12f3" +checksum = "9eb6ea5562eeaed6936b8b54e086aa0f88b9e5b1bef45beb038e2519fa1185b1" dependencies = [ "cfg-if", "libc", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -9980,7 +9982,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ - "dirs 5.0.1", + "dirs 6.0.0", ] [[package]] @@ -10312,7 +10314,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10577,6 +10579,12 @@ dependencies = [ "zeno", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -10818,7 +10826,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -11429,6 +11437,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -12797,7 +12818,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0a8a73a016..f6684e34c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ members = [ "crates/er_flow", "crates/terminal", "crates/terminal_view", - "crates/ferrum-flow", "main", "crates/ssh", "crates/sftp", "crates/sftp_view", "crates/one_ui", "crates/redis_view", "crates/mongodb_view", "crates/remote_file_editor"] resolver = "2" @@ -157,7 +156,7 @@ interprocess = { version = "2.4.0", features = ["tokio"] } url = "2.5.4" percent-encoding = "2.3.1" global-hotkey = "0.7.0" -ferrum-flow = { path = "crates/ferrum-flow" } +ferrum-flow = { git = "https://github.com/tu6ge/ferrum-flow.git", rev = "507cab7505b8dfa6a1e4a121e3c503e3f2f882d6" } [patch.crates-io] gpui = { git = "https://github.com/zed-industries/zed", rev = "8b5328ca" } diff --git a/README.md b/README.md index dbe18679d2..992499482e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ **Edit files directly from the app, with syntax highlighting and autocomplete.** ![remote_file_editor](remote_file_editor.png) +**ER Diagram** +![ER Diagram](er.png) + +Thanks to [ferrum-flow](https://github.com/tu6ge/ferrum-flow.git). + ## Platform Support | Platform | Architecture | Rendering | diff --git a/README_CN.md b/README_CN.md index 4ccbd854f7..396b268381 100644 --- a/README_CN.md +++ b/README_CN.md @@ -75,6 +75,11 @@ **直接在应用程序中编辑文件,具备语法高亮显示和自动完成功能。** ![remote_file_editor](remote_file_editor.png) +**ER 图** +![ER 图](er.png) + +感谢 [ferrum-flow](https://github.com/tu6ge/ferrum-flow.git)。 + ## 平台支持 | 平台 | 架构 | 渲染后端 | diff --git a/crates/core/src/ai_chat/components/model_settings.rs b/crates/core/src/ai_chat/components/model_settings.rs index 1981f39ece..ef4ae145d5 100644 --- a/crates/core/src/ai_chat/components/model_settings.rs +++ b/crates/core/src/ai_chat/components/model_settings.rs @@ -137,8 +137,6 @@ impl ModelSettingsPanel { Self::with_labels(settings, ModelSettingsLabels::default(), window, cx) } - - /// 使用自定义标签创建模型设置面板 pub fn with_labels( settings: ModelSettings, diff --git a/crates/core/src/popup_window.rs b/crates/core/src/popup_window.rs index 9d1fcee885..a00e5c0b3f 100644 --- a/crates/core/src/popup_window.rs +++ b/crates/core/src/popup_window.rs @@ -283,6 +283,7 @@ pub fn open_popup_window_with_should_close( let title = options.title.clone(); let on_should_close = Arc::new(on_should_close); let kind = options.kind.clone(); + let corner_radius = cx.theme().radius_lg; cx.spawn(async move |cx| { let on_should_close = Arc::clone(&on_should_close); @@ -314,7 +315,15 @@ pub fn open_popup_window_with_should_close( let view = create_view_fn(window, cx).into(); let popup_view = cx.new(|cx| PopupWindowView::new(view, Some(content_size), window, cx)); - cx.new(|cx| Root::new(popup_view, window, cx)) + cx.new(|cx| { + let mut root = Root::new(popup_view, window, cx); + #[cfg(target_os = "linux")] + { + // popup 的可见底色由 PopupWindowView 承担,避免 Root 底色在圆角处透出。 + root = root.bg(gpui::transparent_black()); + } + root + }) })?; window.update(cx, |_, window, _| { diff --git a/crates/core/src/storage/models.rs b/crates/core/src/storage/models.rs index 8c7586c775..b83c7e9c00 100644 --- a/crates/core/src/storage/models.rs +++ b/crates/core/src/storage/models.rs @@ -199,6 +199,9 @@ pub struct SshParams { /// SFTP 远程目录(留空则使用服务器默认目录) #[serde(skip_serializing_if = "Option::is_none")] pub sftp_remote_directory: Option, + /// 关闭 shell integration 注入(走裸 request_shell,牺牲 prompt hook / 命令记录 / vim 鼠标) + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_shell_integration: Option, /// 跳板机配置 #[serde(skip_serializing_if = "Option::is_none")] pub jump_server: Option, diff --git a/crates/db/src/clickhouse/plugin.rs b/crates/db/src/clickhouse/plugin.rs index 50fddcfebd..c555c13796 100644 --- a/crates/db/src/clickhouse/plugin.rs +++ b/crates/db/src/clickhouse/plugin.rs @@ -18,8 +18,8 @@ use crate::manifest_helpers::{ use crate::plugin::{DatabaseOperationRequest, DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiCapabilities, - DatabaseUiManifest, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, }; use crate::types::*; @@ -505,6 +505,15 @@ impl DatabasePlugin for ClickHousePlugin { format!("`{}`", identifier.replace("`", "``")) } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_functions: true, + supports_table_engine: true, + table_engines: self.engines(), + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { CLICKHOUSE_UI_MANIFEST.clone() } @@ -688,10 +697,6 @@ impl DatabasePlugin for ClickHousePlugin { } } - fn supports_sequences(&self) -> bool { - false - } - // === Database/Schema Level Operations === fn sql_dialect(&self) -> Box { @@ -1112,10 +1117,6 @@ impl DatabasePlugin for ClickHousePlugin { // === Function Operations === - fn supports_procedures(&self) -> bool { - false - } - async fn list_procedures( &self, _connection: &dyn DbConnection, @@ -1585,6 +1586,15 @@ mod tests { assert_eq!(plugin.quote_identifier("col`umn"), "`col``umn`"); } + #[test] + fn test_capabilities() { + let capabilities = create_plugin().capabilities(); + assert!(capabilities.supports_functions); + assert!(!capabilities.supports_procedures); + assert!(!capabilities.supports_sequences); + assert_eq!(capabilities.table_engines, clickhouse_engine_names()); + } + #[test] fn test_ui_manifest_smoke() { let manifest = create_plugin().ui_manifest(); diff --git a/crates/db/src/duckdb/plugin.rs b/crates/db/src/duckdb/plugin.rs index b58cf3ab84..055f5b6185 100644 --- a/crates/db/src/duckdb/plugin.rs +++ b/crates/db/src/duckdb/plugin.rs @@ -17,7 +17,8 @@ use crate::manifest_helpers::{DatabaseActionDescriptorExt, action, action_with_s use crate::plugin::{DatabaseOperationRequest, DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiManifest, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, }; use crate::sqlite::SqlitePlugin; use crate::types::*; @@ -753,6 +754,10 @@ impl DatabasePlugin for DuckDbPlugin { DatabaseType::DuckDB } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities::default() + } + fn ui_manifest(&self) -> DatabaseUiManifest { DUCKDB_UI_MANIFEST.clone() } @@ -1192,10 +1197,6 @@ impl DatabasePlugin for DuckDbPlugin { }) } - fn supports_functions(&self) -> bool { - false - } - async fn list_functions( &self, connection: &dyn DbConnection, @@ -1212,10 +1213,6 @@ impl DatabasePlugin for DuckDbPlugin { self.sqlite.list_functions_view(connection, database).await } - fn supports_procedures(&self) -> bool { - false - } - async fn list_procedures( &self, connection: &dyn DbConnection, @@ -1468,6 +1465,14 @@ mod tests { DuckDbPlugin::new() } + #[test] + fn test_capabilities() { + let capabilities = create_plugin().capabilities(); + assert!(!capabilities.supports_functions); + assert!(!capabilities.supports_procedures); + assert!(!capabilities.supports_sequences); + } + #[test] fn test_ui_manifest_smoke() { let manifest = create_plugin().ui_manifest(); diff --git a/crates/db/src/import_export/formats/csv.rs b/crates/db/src/import_export/formats/csv.rs index e6a75298cc..f6cc0c94a2 100644 --- a/crates/db/src/import_export/formats/csv.rs +++ b/crates/db/src/import_export/formats/csv.rs @@ -1,19 +1,16 @@ use std::time::Instant; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_trait::async_trait; -use super::{ - build_export_select_sql, build_insert_statement, execute_import_statements, - format_import_table_reference, quote_sql_string, -}; +use super::format_import_table_reference; +use crate::DatabasePlugin; use crate::connection::DbConnection; use crate::executor::{ExecOptions, SqlResult}; use crate::import_export::{ ExportConfig, ExportProgressEvent, ExportProgressSender, ExportResult, FormatHandler, ImportConfig, ImportResult, }; -use crate::DatabasePlugin; pub struct CsvFormatHandler; @@ -108,11 +105,15 @@ impl CsvFormatHandler { } } - fn sql_literal_from_option(value: &Option) -> String { + fn append_sql_value(insert_sql: &mut String, value: &Option) { match value { - None => "NULL".to_string(), - Some(v) if v.eq_ignore_ascii_case("null") => "NULL".to_string(), - Some(v) => quote_sql_string(v), + None => insert_sql.push_str("NULL"), + Some(v) if v.eq_ignore_ascii_case("null") => insert_sql.push_str("NULL"), + Some(v) => { + insert_sql.push('\''); + insert_sql.push_str(&v.replace('\'', "''")); + insert_sql.push('\''); + } } } } @@ -195,7 +196,6 @@ impl FormatHandler for CsvFormatHandler { } } - let mut statements = Vec::new(); for (record_num, values) in records.iter().skip(data_start_record).enumerate() { let record_number = record_num + data_start_record + 1; if values.len() != columns.len() { @@ -206,33 +206,49 @@ impl FormatHandler for CsvFormatHandler { continue; } - let sql_values = values - .iter() - .map(Self::sql_literal_from_option) - .collect::>(); - statements.push(( - record_number, - build_insert_statement(plugin, &table_ref, &columns, &sql_values), - )); - } + let mut insert_sql = format!("INSERT INTO {} (", table_ref); + for (i, col) in columns.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); + } + insert_sql.push_str(&plugin.quote_identifier(col)); + } + insert_sql.push_str(") VALUES ("); - let statement_sql = statements - .iter() - .map(|(_, sql)| sql.clone()) - .collect::>(); - let results = execute_import_statements(plugin, connection, config, &statement_sql).await?; - for ((record_number, _), result) in statements.iter().zip(results.into_iter()) { - match result { - SqlResult::Exec(exec_result) => { - total_rows += exec_result.rows_affected; + for (i, val) in values.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); } - SqlResult::Error(err) => { - errors.push(format!("Record {}: {}", record_number, err.message)); + Self::append_sql_value(&mut insert_sql, val); + } + insert_sql.push(')'); + + match connection + .execute(plugin, &insert_sql, ExecOptions::default()) + .await + { + Ok(results) => { + for result in results { + match result { + SqlResult::Exec(exec_result) => { + total_rows += exec_result.rows_affected; + } + SqlResult::Error(err) => { + errors.push(format!("Record {}: {}", record_number, err.message)); + if config.stop_on_error { + break; + } + } + _ => {} + } + } + } + Err(e) => { + errors.push(format!("Record {}: {}", record_number, e)); if config.stop_on_error { break; } } - _ => {} } } @@ -290,7 +306,26 @@ impl FormatHandler for CsvFormatHandler { table: table.clone(), }); - let select_sql = build_export_select_sql(plugin, config, table); + let table_ref = plugin.format_table_reference(&config.database, None, table); + let columns_str = if let Some(cols) = &config.columns { + cols.iter() + .map(|c| plugin.quote_identifier(c)) + .collect::>() + .join(", ") + } else { + "*".to_string() + }; + + let mut select_sql = format!("SELECT {} FROM {}", columns_str, table_ref); + if let Some(where_clause) = &config.where_clause { + select_sql.push_str(" WHERE "); + select_sql.push_str(where_clause); + } + if let Some(limit) = config.limit { + let pagination = plugin.format_pagination(limit, 0, ""); + select_sql.push_str(&pagination); + } + let result = connection .query(&select_sql) .await @@ -363,19 +398,21 @@ mod tests { #[test] fn test_append_sql_value_formats_option_string_correctly() { - assert_eq!(CsvFormatHandler::sql_literal_from_option(&None), "NULL"); - assert_eq!( - CsvFormatHandler::sql_literal_from_option(&Some(String::new())), - "''" - ); - assert_eq!( - CsvFormatHandler::sql_literal_from_option(&Some("null".to_string())), - "NULL" - ); - assert_eq!( - CsvFormatHandler::sql_literal_from_option(&Some("O'Reilly".to_string())), - "'O''Reilly'" - ); + let mut sql = String::new(); + CsvFormatHandler::append_sql_value(&mut sql, &None); + assert_eq!(sql, "NULL"); + + sql.clear(); + CsvFormatHandler::append_sql_value(&mut sql, &Some(String::new())); + assert_eq!(sql, "''"); + + sql.clear(); + CsvFormatHandler::append_sql_value(&mut sql, &Some("null".to_string())); + assert_eq!(sql, "NULL"); + + sql.clear(); + CsvFormatHandler::append_sql_value(&mut sql, &Some("O'Reilly".to_string())); + assert_eq!(sql, "'O''Reilly'"); } #[test] diff --git a/crates/db/src/import_export/formats/json.rs b/crates/db/src/import_export/formats/json.rs index 65e449a89f..a330d584ca 100644 --- a/crates/db/src/import_export/formats/json.rs +++ b/crates/db/src/import_export/formats/json.rs @@ -1,20 +1,17 @@ use std::time::Instant; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_trait::async_trait; use serde_json::Value; -use super::{ - build_export_select_sql, build_insert_statement, execute_import_statements, - format_import_table_reference, quote_sql_string, -}; +use super::format_import_table_reference; +use crate::DatabasePlugin; use crate::connection::DbConnection; use crate::executor::{ExecOptions, SqlResult}; use crate::import_export::{ ExportConfig, ExportProgressEvent, ExportProgressSender, ExportResult, FormatHandler, ImportConfig, ImportResult, }; -use crate::DatabasePlugin; pub struct JsonFormatHandler; @@ -83,13 +80,12 @@ impl FormatHandler for JsonFormatHandler { .ok_or_else(|| anyhow!("JSON array must contain objects"))?; let columns: Vec = first_obj.keys().cloned().collect(); - let mut statements = Vec::new(); - for (index, row_obj) in rows.iter().enumerate() { - let row_number = index + 1; + // 批量插入 + for row_obj in rows { let obj = match row_obj.as_object() { Some(o) => o, None => { - errors.push(format!("Row {}: row is not an object", row_number)); + errors.push("Row is not an object".to_string()); if config.stop_on_error { break; } @@ -97,45 +93,63 @@ impl FormatHandler for JsonFormatHandler { } }; - let sql_values = columns - .iter() - .map(|col| match obj.get(col) { - Some(Value::Null) | None => "NULL".to_string(), - Some(Value::String(s)) => quote_sql_string(s), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => { - if *b { - "1".to_string() - } else { - "0".to_string() - } + let mut insert_sql = format!("INSERT INTO {} (", table_ref); + for (i, col) in columns.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); + } + insert_sql.push_str(&plugin.quote_identifier(col)); + } + insert_sql.push_str(") VALUES ("); + + for (i, col) in columns.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); + } + match obj.get(col) { + Some(Value::Null) | None => insert_sql.push_str("NULL"), + Some(Value::String(s)) => { + insert_sql.push('\''); + insert_sql.push_str(&s.replace('\'', "''")); + insert_sql.push('\''); } - Some(v) => quote_sql_string(&v.to_string()), - }) - .collect::>(); - statements.push(( - row_number, - build_insert_statement(plugin, &table_ref, &columns, &sql_values), - )); - } + Some(Value::Number(n)) => insert_sql.push_str(&n.to_string()), + Some(Value::Bool(b)) => insert_sql.push_str(if *b { "1" } else { "0" }), + Some(v) => { + insert_sql.push('\''); + insert_sql.push_str(&v.to_string().replace('\'', "''")); + insert_sql.push('\''); + } + } + } + insert_sql.push(')'); - let statement_sql = statements - .iter() - .map(|(_, sql)| sql.clone()) - .collect::>(); - let results = execute_import_statements(plugin, connection, config, &statement_sql).await?; - for ((row_number, _), result) in statements.iter().zip(results.into_iter()) { - match result { - SqlResult::Exec(exec_result) => { - total_rows += exec_result.rows_affected; + match connection + .execute(plugin, &insert_sql, ExecOptions::default()) + .await + { + Ok(results) => { + for result in results { + match result { + SqlResult::Exec(exec_result) => { + total_rows += exec_result.rows_affected; + } + SqlResult::Error(err) => { + errors.push(format!("Insert failed: {}", err.message)); + if config.stop_on_error { + break; + } + } + _ => {} + } + } } - SqlResult::Error(err) => { - errors.push(format!("Row {}: {}", row_number, err.message)); + Err(e) => { + errors.push(format!("Insert failed: {}", e)); if config.stop_on_error { break; } } - _ => {} } } @@ -187,7 +201,26 @@ impl FormatHandler for JsonFormatHandler { table: table.clone(), }); - let select_sql = build_export_select_sql(plugin, config, table); + let table_ref = plugin.format_table_reference(&config.database, None, table); + let columns_str = if let Some(cols) = &config.columns { + cols.iter() + .map(|c| plugin.quote_identifier(c)) + .collect::>() + .join(", ") + } else { + "*".to_string() + }; + + let mut select_sql = format!("SELECT {} FROM {}", columns_str, table_ref); + if let Some(where_clause) = &config.where_clause { + select_sql.push_str(" WHERE "); + select_sql.push_str(where_clause); + } + if let Some(limit) = config.limit { + let pagination = plugin.format_pagination(limit, 0, ""); + select_sql.push_str(&pagination); + } + let result = connection .query(&select_sql) .await diff --git a/crates/db/src/import_export/formats/mod.rs b/crates/db/src/import_export/formats/mod.rs index 1733103779..15e12e29ab 100644 --- a/crates/db/src/import_export/formats/mod.rs +++ b/crates/db/src/import_export/formats/mod.rs @@ -1,8 +1,5 @@ -use crate::connection::DbConnection; -use crate::executor::{ExecOptions, SqlResult}; -use crate::import_export::{ExportConfig, ImportConfig}; use crate::DatabasePlugin; -use anyhow::{anyhow, Result}; +use crate::import_export::ImportConfig; pub mod csv; pub mod json; @@ -24,90 +21,10 @@ pub(super) fn format_import_table_reference( plugin.format_table_reference(&config.database, config.schema.as_deref(), table) } -pub(super) fn build_export_select_sql( - plugin: &dyn DatabasePlugin, - config: &ExportConfig, - table: &str, -) -> String { - let table_ref = - plugin.format_table_reference(&config.database, config.schema.as_deref(), table); - let columns_str = if let Some(cols) = &config.columns { - cols.iter() - .map(|c| plugin.quote_identifier(c)) - .collect::>() - .join(", ") - } else { - "*".to_string() - }; - - let mut select_sql = format!("SELECT {} FROM {}", columns_str, table_ref); - if let Some(where_clause) = &config.where_clause { - select_sql.push_str(" WHERE "); - select_sql.push_str(where_clause); - } - if let Some(limit) = config.limit { - let pagination = plugin.format_pagination(limit, 0, ""); - select_sql.push_str(&pagination); - } - select_sql -} - -pub(super) fn build_insert_statement( - plugin: &dyn DatabasePlugin, - table_ref: &str, - columns: &[String], - sql_values: &[String], -) -> String { - let mut sql = format!("INSERT INTO {} (", table_ref); - for (i, col) in columns.iter().enumerate() { - if i > 0 { - sql.push_str(", "); - } - sql.push_str(&plugin.quote_identifier(col)); - } - sql.push_str(") VALUES ("); - for (i, value) in sql_values.iter().enumerate() { - if i > 0 { - sql.push_str(", "); - } - sql.push_str(value); - } - sql.push(')'); - sql -} - -pub(super) fn quote_sql_string(value: &str) -> String { - format!("'{}'", value.replace('\'', "''")) -} - -pub(super) async fn execute_import_statements( - plugin: &dyn DatabasePlugin, - connection: &dyn DbConnection, - config: &ImportConfig, - statements: &[String], -) -> Result> { - if statements.is_empty() { - return Ok(Vec::new()); - } - - let script = statements.join(";\n"); - let options = ExecOptions { - stop_on_error: config.stop_on_error, - transactional: config.use_transaction, - max_rows: None, - streaming: false, - }; - - connection - .execute(plugin, &script, options) - .await - .map_err(|e| anyhow!("Import failed: {}", e)) -} - #[cfg(test)] mod tests { - use super::{build_export_select_sql, format_import_table_reference}; - use crate::import_export::{ExportConfig, ImportConfig}; + use super::format_import_table_reference; + use crate::import_export::ImportConfig; use crate::mssql::MsSqlPlugin; use crate::mysql::MySqlPlugin; @@ -139,25 +56,4 @@ mod tests { assert_eq!(table_ref, "[warehouse].[sales].[orders]"); } - - #[test] - fn test_build_export_select_sql_keeps_schema_where_and_limit() { - let plugin = MsSqlPlugin::new(); - let config = ExportConfig { - database: "warehouse".to_string(), - schema: Some("sales".to_string()), - tables: vec!["orders".to_string()], - columns: Some(vec!["id".to_string(), "name".to_string()]), - where_clause: Some("status = 1".to_string()), - limit: Some(10), - ..ExportConfig::default() - }; - - let sql = build_export_select_sql(&plugin, &config, "orders"); - - assert_eq!( - sql, - "SELECT [id], [name] FROM [warehouse].[sales].[orders] WHERE status = 1 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY" - ); - } } diff --git a/crates/db/src/import_export/formats/txt.rs b/crates/db/src/import_export/formats/txt.rs index 15f607756d..e3b36bfd44 100644 --- a/crates/db/src/import_export/formats/txt.rs +++ b/crates/db/src/import_export/formats/txt.rs @@ -1,19 +1,16 @@ use std::time::Instant; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_trait::async_trait; -use super::{ - build_export_select_sql, build_insert_statement, execute_import_statements, - format_import_table_reference, quote_sql_string, -}; +use super::format_import_table_reference; +use crate::DatabasePlugin; use crate::connection::DbConnection; use crate::executor::{ExecOptions, SqlResult}; use crate::import_export::{ ExportConfig, ExportProgressEvent, ExportProgressSender, ExportResult, FormatHandler, ImportConfig, ImportResult, }; -use crate::DatabasePlugin; pub struct TxtFormatHandler; @@ -93,7 +90,6 @@ impl FormatHandler for TxtFormatHandler { } } - let mut statements = Vec::new(); for (line_num, line) in lines.iter().skip(1).enumerate() { if line.trim().is_empty() { continue; @@ -108,39 +104,55 @@ impl FormatHandler for TxtFormatHandler { continue; } - let sql_values = values - .iter() - .map(|val| { - if val.is_empty() || val.eq_ignore_ascii_case("null") { - "NULL".to_string() - } else { - quote_sql_string(val) - } - }) - .collect::>(); - statements.push(( - line_num + 2, - build_insert_statement(plugin, &table_ref, &columns, &sql_values), - )); - } + let mut insert_sql = format!("INSERT INTO {} (", table_ref); + for (i, col) in columns.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); + } + insert_sql.push_str(&plugin.quote_identifier(col)); + } + insert_sql.push_str(") VALUES ("); - let statement_sql = statements - .iter() - .map(|(_, sql)| sql.clone()) - .collect::>(); - let results = execute_import_statements(plugin, connection, config, &statement_sql).await?; - for ((line_number, _), result) in statements.iter().zip(results.into_iter()) { - match result { - SqlResult::Exec(exec_result) => { - total_rows += exec_result.rows_affected; + for (i, val) in values.iter().enumerate() { + if i > 0 { + insert_sql.push_str(", "); + } + if val.is_empty() || val.eq_ignore_ascii_case("null") { + insert_sql.push_str("NULL"); + } else { + insert_sql.push('\''); + insert_sql.push_str(&val.replace('\'', "''")); + insert_sql.push('\''); } - SqlResult::Error(err) => { - errors.push(format!("Line {}: {}", line_number, err.message)); + } + insert_sql.push(')'); + + match connection + .execute(plugin, &insert_sql, ExecOptions::default()) + .await + { + Ok(results) => { + for result in results { + match result { + SqlResult::Exec(exec_result) => { + total_rows += exec_result.rows_affected; + } + SqlResult::Error(err) => { + errors.push(format!("Line {}: {}", line_num + 2, err.message)); + if config.stop_on_error { + break; + } + } + _ => {} + } + } + } + Err(e) => { + errors.push(format!("Line {}: {}", line_num + 2, e)); if config.stop_on_error { break; } } - _ => {} } } @@ -207,7 +219,26 @@ impl FormatHandler for TxtFormatHandler { table: table.clone(), }); - let select_sql = build_export_select_sql(plugin, config, table); + let table_ref = plugin.format_table_reference(&config.database, None, table); + let columns_str = if let Some(cols) = &config.columns { + cols.iter() + .map(|c| plugin.quote_identifier(c)) + .collect::>() + .join(", ") + } else { + "*".to_string() + }; + + let mut select_sql = format!("SELECT {} FROM {}", columns_str, table_ref); + if let Some(where_clause) = &config.where_clause { + select_sql.push_str(" WHERE "); + select_sql.push_str(where_clause); + } + if let Some(limit) = config.limit { + let pagination = plugin.format_pagination(limit, 0, ""); + select_sql.push_str(&pagination); + } + let result = connection .query(&select_sql) .await diff --git a/crates/db/src/import_export/formats/xml.rs b/crates/db/src/import_export/formats/xml.rs index 40035f620c..72f40a1df3 100644 --- a/crates/db/src/import_export/formats/xml.rs +++ b/crates/db/src/import_export/formats/xml.rs @@ -1,16 +1,15 @@ use std::time::Instant; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_trait::async_trait; -use super::build_export_select_sql; +use crate::DatabasePlugin; use crate::connection::DbConnection; use crate::executor::SqlResult; use crate::import_export::{ ExportConfig, ExportProgressEvent, ExportProgressSender, ExportResult, FormatHandler, ImportConfig, ImportResult, }; -use crate::DatabasePlugin; pub struct XmlFormatHandler; @@ -105,7 +104,26 @@ impl FormatHandler for XmlFormatHandler { table: table.clone(), }); - let select_sql = build_export_select_sql(plugin, config, table); + let table_ref = plugin.format_table_reference(&config.database, None, table); + let columns_str = if let Some(cols) = &config.columns { + cols.iter() + .map(|c| plugin.quote_identifier(c)) + .collect::>() + .join(", ") + } else { + "*".to_string() + }; + + let mut select_sql = format!("SELECT {} FROM {}", columns_str, table_ref); + if let Some(where_clause) = &config.where_clause { + select_sql.push_str(" WHERE "); + select_sql.push_str(where_clause); + } + if let Some(limit) = config.limit { + let pagination = plugin.format_pagination(limit, 0, ""); + select_sql.push_str(&pagination); + } + let result = connection .query(&select_sql) .await diff --git a/crates/db/src/ipc/client.rs b/crates/db/src/ipc/client.rs index 9db1ad2664..fda3d5f1c8 100644 --- a/crates/db/src/ipc/client.rs +++ b/crates/db/src/ipc/client.rs @@ -5,54 +5,106 @@ use interprocess::local_socket::{ tokio::{Stream as LocalSocketStream, prelude::*}, }; use ipc::{ - IpcRequest, IpcResponse, + IpcErrorCode, IpcRequest, IpcResponse, framing::{recv_msg_async, send_msg_async}, }; use serde::de::DeserializeOwned; use serde_json::Value; +use std::collections::HashMap; use std::process::Stdio; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::Duration; -use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::io::{AsyncBufReadExt, BufReader, ReadHalf, WriteHalf, split}; use tokio::process::{Child, Command}; -use tokio::time::{Instant, error::Elapsed, sleep, timeout}; +use tokio::sync::{Mutex, oneshot}; +use tokio::task::JoinHandle; +use tokio::time::{Instant, sleep, timeout}; use tracing::warn; const REQUEST_TIMEOUT_MS: u64 = 30_000; +/// 通过该环境变量把 client 生成的动态 socket 名透传给 driver 子进程。 +/// +/// driver 启动时优先读这个变量来决定 listen 名,从而支持「同 driver 多实例」 +/// 场景:每个 ExternalDbConnection 都拿到独立的 socket,互不冲突。 +pub const SOCKET_ENV_VAR: &str = "ONETCLI_IPC_SOCKET"; + +/// 客户端「写半 / 路由表 / 关闭标记」共享状态。 +/// +/// - `writer`: tokio::sync::Mutex 串行化「写一帧」操作。 +/// - `pending`: std::sync::Mutex 持锁时间极短(insert/remove HashMap),且允许在 +/// Drop 中同步 lock,这是 cancel-safety 的关键。 +/// - `next_id`: AtomicU64,无锁分配 request id。 +/// - `closed`: AtomicBool,reader task 退出后置位,后续 caller 立即失败。 +struct ClientShared { + writer: Mutex>, + pending: StdMutex>>, + next_id: AtomicU64, + closed: AtomicBool, +} + +/// JSON-RPC over IPC client。 +/// +/// 单 stream 多 caller 并发:writer mutex 串行化写,reader task 把响应按 +/// `request_id` 路由到对应 caller 的 oneshot。caller drop / timeout / 写失败 +/// 均不会泄漏 pending 表条目(由 PendingGuard 的 RAII Drop 保证)。 pub struct JsonRpcClient { - child: Option, - stream: LocalSocketStream, - next_id: u64, + shared: Arc, + reader_task: JoinHandle<()>, + /// 子进程 owner;包在 std Mutex 里以让 `JsonRpcClient: Sync`。 + /// `kill_on_drop=true` 保证 child 被 drop 时进程被 OS 回收。 + child: StdMutex>, } impl JsonRpcClient { pub async fn start(driver: &IpcDriverManifest) -> Result { + // command 为空 → 测试 / 预 listen 模式:server 已绑定 transport.name,直接连。 + // 否则 → 生产模式:每实例生成独立 socket 名,通过 env var 透传给 driver。 + let socket_name = if driver.entry.command.trim().is_empty() { + driver.transport.name.clone() + } else { + make_socket_name(driver) + }; + let mut child = if driver.entry.command.trim().is_empty() { None } else { - Some(spawn_driver_process(driver).await?) - }; - let stream = match connect_local_socket( - &driver.transport.name, - driver.transport.connect_timeout_ms(), - ) - .await - { - Ok(stream) => stream, - Err(error) => { - shutdown_child(&mut child).await; - return Err(error); - } + Some(spawn_driver_process(driver, &socket_name).await?) }; + let stream = + match connect_local_socket(&socket_name, driver.transport.connect_timeout_ms()).await { + Ok(stream) => stream, + Err(error) => { + shutdown_child(&mut child).await; + return Err(error); + } + }; + + let (read_half, write_half) = split(stream); + + let shared = Arc::new(ClientShared { + writer: Mutex::new(write_half), + pending: StdMutex::new(HashMap::new()), + next_id: AtomicU64::new(1), + closed: AtomicBool::new(false), + }); + + let reader_shared = Arc::clone(&shared); + let reader_task = tokio::spawn(async move { + reader_loop(read_half, reader_shared).await; + }); + Ok(Self { - child, - stream, - next_id: 1, + shared, + reader_task, + child: StdMutex::new(child), }) } - pub async fn request(&mut self, method: &str, params: Value) -> Result + pub async fn request(&self, method: &str, params: Value) -> Result where T: DeserializeOwned, { @@ -61,27 +113,144 @@ impl JsonRpcClient { .map_err(|error| DbError::query_with_source("invalid external driver response", error)) } - pub async fn request_value(&mut self, method: &str, params: Value) -> Result { - let id = self.next_id; - self.next_id = self.next_id.saturating_add(1); + pub async fn request_value(&self, method: &str, params: Value) -> Result { + if self.shared.closed.load(Ordering::Acquire) { + return Err(DbError::connection("driver disconnected")); + } + + let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = oneshot::channel(); + + // 注册 pending,double-check closed 防止 reader 已 drain。 + { + let mut pending = self.shared.pending.lock().expect("pending mutex poisoned"); + if self.shared.closed.load(Ordering::Acquire) { + return Err(DbError::connection("driver disconnected")); + } + pending.insert(id, tx); + } + + // RAII guard:future cancel / timeout / 写失败时从 pending 拿掉 sender,避免泄漏。 + let mut guard = PendingGuard { + shared: Arc::clone(&self.shared), + id, + armed: true, + }; + + // 写一帧;writer mutex 仅在写期间持锁,写完立刻释放允许下个 caller 写。 let request = IpcRequest::new(id, method, params); + let send_result = { + let mut writer = self.shared.writer.lock().await; + send_msg_async(&mut *writer, &request).await + }; + if let Err(error) = send_result { + return Err(DbError::query_with_source( + "failed to write IPC request", + error, + )); + // guard.drop → remove pending entry + } - send_msg_async(&mut self.stream, &request) - .await - .map_err(|error| DbError::query_with_source("failed to write IPC request", error))?; + // 等回复。 + match timeout(Duration::from_millis(REQUEST_TIMEOUT_MS), rx).await { + Ok(Ok(response)) => { + guard.armed = false; // reader 已 take 走 sender,不需再清理 + validate_response(response, id) + } + Ok(Err(_)) => { + guard.armed = false; // reader 关闭已 drain pending + Err(DbError::connection("driver disconnected")) + } + Err(_) => { + // timeout:guard.drop 清理 sender,reader 后到的 response 静默丢弃 + Err(DbError::query("timed out waiting for IPC response")) + } + } + } - timeout( - Duration::from_millis(REQUEST_TIMEOUT_MS), - recv_msg_async::<_, IpcResponse>(&mut self.stream), - ) - .await - .map_err(request_timeout_error)? - .map_err(|error| DbError::query_with_source("failed to read IPC response", error)) - .and_then(|response| validate_response(response, id)) + /// 显式关闭:abort reader,kill + wait child。 + /// 通常在 ExternalDbConnection::disconnect 末尾调用,确保子进程退出后才返回。 + pub async fn shutdown(&self) { + close_and_drain(&self.shared); + self.reader_task.abort(); + let mut taken = { + let mut guard = self.child.lock().expect("child mutex poisoned"); + guard.take() + }; + shutdown_child(&mut taken).await; + } + + /// reader task 是否已经退出(stream EOF / error / abort)。 + /// + /// 一旦置位,所有后续 `request` 调用都会立即得到 disconnected 错误。 + /// ExternalDbConnection 用这个信号触发 client eviction(P0-4)。 + pub fn is_closed(&self) -> bool { + self.shared.closed.load(Ordering::Acquire) + } +} + +impl Drop for JsonRpcClient { + fn drop(&mut self) { + // 兜底:abort reader task,child 由 kill_on_drop=true 自动回收。 + // 不在 Drop 里 await,避免阻塞 runtime。 + self.reader_task.abort(); + } +} + +/// RAII 保护 pending 表条目的 cancel-safety。 +/// +/// 当 caller 的 future 被 cancel / timeout / 写失败时,Drop 自动移除 pending sender, +/// 避免内存泄漏与 reader 找不到对应 caller 时的隐性丢弃。 +struct PendingGuard { + shared: Arc, + id: u64, + armed: bool, +} + +impl Drop for PendingGuard { + fn drop(&mut self) { + if !self.armed { + return; + } + if let Ok(mut pending) = self.shared.pending.lock() { + pending.remove(&self.id); + } + } +} + +fn close_and_drain(shared: &ClientShared) { + shared.closed.store(true, Ordering::Release); + if let Ok(mut pending) = shared.pending.lock() { + pending.clear(); + // oneshot::Sender 被 drop → caller 的 rx 收 RecvError → 报 disconnected + } +} + +async fn reader_loop(mut reader: ReadHalf, shared: Arc) { + /// 无论 reader_loop 怎么退出(EOF / Err / task abort),都标记 closed + drain + /// pending,把所有 caller 唤醒为 disconnected。 + struct CloseGuard { + shared: Arc, + } + impl Drop for CloseGuard { + fn drop(&mut self) { + close_and_drain(&self.shared); + } } + let _guard = CloseGuard { + shared: Arc::clone(&shared), + }; - pub async fn shutdown(&mut self) { - shutdown_child(&mut self.child).await; + while let Ok(response) = recv_msg_async::<_, IpcResponse>(&mut reader).await { + let sender = match shared.pending.lock() { + Ok(mut pending) => pending.remove(&response.request_id), + Err(_) => break, // pending mutex poisoned — 走 CloseGuard 兜底 + }; + if let Some(sender) = sender { + // caller 已超时 / cancel drop 了 rx 时 send 失败 — 静默忽略 + let _ = sender.send(response); + } + // 找不到 sender:caller 已 timeout / cancel,response 静默丢弃 } } @@ -101,6 +270,9 @@ fn validate_response(response: IpcResponse, expected_id: u64) -> Result) { } } -fn request_timeout_error(error: Elapsed) -> DbError { - DbError::query_with_source("timed out waiting for IPC response", error) +/// 为 driver 生成本次启动的最终 socket 名。 +/// +/// 使用短前缀避免 macOS `sockaddr_un.sun_path` 容量限制。 +fn make_socket_name(driver: &IpcDriverManifest) -> String { + format!( + "onetcli-{}-{}.sock", + driver.id, + uuid::Uuid::new_v4().simple() + ) } -async fn spawn_driver_process(driver: &IpcDriverManifest) -> Result { +/// 构造 driver 启动 Command,设置 `ONETCLI_IPC_SOCKET` env var 把动态 socket +/// 名透传给子进程。抽出独立函数便于在 Drop / multi-instance 测试中验证 env。 +fn build_driver_command(driver: &IpcDriverManifest, socket_name: &str) -> Command { let mut command = Command::new(&driver.entry.command); command .args(&driver.entry.args) + .env(SOCKET_ENV_VAR, socket_name) .current_dir(driver.command_working_dir()) + // 关键:确保 client 异常 drop 时子进程被回收,不变孤儿。 + // 详见 P0-3 改造 — 仅 `Child::kill().await` 不足以应对 panic / runtime abort 场景。 + .kill_on_drop(true) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::piped()); + command +} +async fn spawn_driver_process( + driver: &IpcDriverManifest, + socket_name: &str, +) -> Result { + let mut command = build_driver_command(driver, socket_name); let mut child = command.spawn().map_err(|error| { DbError::connection_with_source( format!("failed to start external driver '{}'", driver.id), @@ -227,4 +419,122 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("boom")); } + + #[test] + fn maps_unsupported_method_to_not_supported() { + let response = IpcResponse::error(7, IpcErrorCode::UnsupportedMethod, "missing"); + let result = validate_response(response, 7); + assert!(matches!(result, Err(DbError::NotSupported(message)) if message == "missing")); + } + + #[test] + fn make_socket_name_generates_distinct_names_with_manifest_prefix() { + let driver = make_test_manifest("driver.sock"); + let first = make_socket_name(&driver); + let second = make_socket_name(&driver); + + assert_ne!(first, second); + assert!(first.starts_with("onetcli-socket-test-")); + assert!(second.starts_with("onetcli-socket-test-")); + assert!(first.ends_with(".sock")); + assert!(second.ends_with(".sock")); + } + + fn make_test_manifest(socket_name: &str) -> IpcDriverManifest { + IpcDriverManifest { + id: "socket-test".into(), + name: "Socket Test".into(), + description: String::new(), + version: String::new(), + entry: crate::ipc::registry::IpcDriverEntry { + command: "sleep".into(), + args: vec!["30".into()], + working_dir: None, + }, + transport: crate::ipc::registry::IpcDriverTransport::local_socket(socket_name), + dialect: Default::default(), + capabilities: None, + ui: Default::default(), + manifest_dir: std::path::PathBuf::from("/tmp"), + } + } +} + +#[cfg(all(test, unix))] +mod lifecycle_tests { + use super::*; + use crate::ipc::registry::{IpcDriverEntry, IpcDriverManifest, IpcDriverTransport}; + use std::path::PathBuf; + use std::time::Duration; + + /// 构造一个跑 `sleep 30` 的 manifest,作为「永远不会主动退出」的 driver 占位。 + fn make_sleep_manifest() -> IpcDriverManifest { + IpcDriverManifest { + id: "lifecycle-test".into(), + name: "Lifecycle Test".into(), + description: String::new(), + version: String::new(), + entry: IpcDriverEntry { + command: "sleep".into(), + args: vec!["30".into()], + working_dir: None, + }, + transport: IpcDriverTransport::local_socket("onetcli-lifecycle-test.sock"), + dialect: Default::default(), + capabilities: None, + ui: Default::default(), + manifest_dir: PathBuf::from("/tmp"), + } + } + + /// 通过 `kill -0 ` 检测 unix 进程是否仍存活。 + fn process_alive(pid: u32) -> bool { + std::process::Command::new("kill") + .args(["-0", &pid.to_string()]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + } + + /// 兜底回收:测试失败时也不要把 sleep 进程留给 CI。 + fn force_kill(pid: u32) { + let _ = std::process::Command::new("kill") + .args(["-9", &pid.to_string()]) + .status(); + } + + #[tokio::test] + async fn spawn_driver_process_kills_child_when_handle_drops() { + let manifest = make_sleep_manifest(); + let socket_name = manifest.transport.name.clone(); + let child = spawn_driver_process(&manifest, &socket_name) + .await + .expect("spawn driver child process"); + let pid = child.id().expect("child pid should be available"); + + assert!( + process_alive(pid), + "child should be alive immediately after spawn" + ); + + drop(child); + + // 给 OS 至多 2 秒时间发送信号并清理 zombie。 + let mut reaped = false; + for _ in 0..20 { + if !process_alive(pid) { + reaped = true; + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + if !reaped { + force_kill(pid); + } + assert!( + reaped, + "child pid={pid} should be killed within 2s after Child handle drops" + ); + } } diff --git a/crates/db/src/ipc/connection.rs b/crates/db/src/ipc/connection.rs index 50036917ee..fe9bd61ebc 100644 --- a/crates/db/src/ipc/connection.rs +++ b/crates/db/src/ipc/connection.rs @@ -8,13 +8,17 @@ use crate::ipc::registry::IpcDriverManifest; use crate::{DatabasePlugin, SqlErrorInfo, truncate_str}; use async_trait::async_trait; use one_core::storage::DbConnectionConfig; +use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; use tracing::{debug, error}; pub struct ExternalDbConnection { config: DbConnectionConfig, driver: IpcDriverManifest, - client: Mutex>, + /// `Arc` 让 `request` 能短锁拿 clone 后立刻释放,允许多 caller 并发调用 + /// `JsonRpcClient::request`。`Mutex>` 处理 connect/disconnect 的 + /// owner 切换。 + client: Mutex>>, } impl ExternalDbConnection { @@ -30,9 +34,29 @@ impl ExternalDbConnection { where T: serde::de::DeserializeOwned, { - let mut guard = self.client.lock().await; - let client = guard.as_mut().ok_or(DbError::NotConnected)?; - client.request(method, params).await + // 短锁:仅在拿 Arc clone 时持锁,之后释放,允许多 caller 并发调用 client。 + let client = { + let guard = self.client.lock().await; + guard.as_ref().cloned().ok_or(DbError::NotConnected)? + }; + + let result = client.request(method, params).await; + + // P0-4:transport 已 fatal(reader task 退出),evict 当前 broken client + // 让下次 request 直接得到 NotConnected,触发上层重连逻辑。 + // 用 Arc::ptr_eq 防止误踩 — 别的 caller 可能已经 evict + reconnect。 + if client.is_closed() { + let mut guard = self.client.lock().await; + if let Some(current) = guard.as_ref() { + if Arc::ptr_eq(current, &client) { + *guard = None; + // 旧 Arc 在最后一个 in-flight reference drop 后才真正释放, + // 由 JsonRpcClient::Drop 完成 reader_task abort + kill_on_drop child。 + } + } + } + + result } } @@ -57,21 +81,27 @@ impl DbConnection for ExternalDbConnection { } async fn connect(&mut self) -> Result<(), DbError> { - let mut client = JsonRpcClient::start(&self.driver).await?; + let client = JsonRpcClient::start(&self.driver).await?; + // initialize / connect 走 `&self`,这里直接用 owned client(尚未 Arc), + // 任一步失败就把 client drop 掉 → reader_task abort + child kill_on_drop。 let _: serde_json::Value = client.request("initialize", empty_params()).await?; let _: serde_json::Value = client .request("connect", connection_config_params(&self.config)) .await?; - *self.client.lock().await = Some(client); + *self.client.lock().await = Some(Arc::new(client)); Ok(()) } async fn disconnect(&mut self) -> Result<(), DbError> { - let mut client = self.client.lock().await.take(); - if let Some(client) = client.as_mut() { + let client_arc = self.client.lock().await.take(); + if let Some(client_arc) = client_arc { + // 尽力发出 disconnect RPC(可能因连接已断而失败,允许)。 let _: Result = - client.request("disconnect", empty_params()).await; - client.shutdown().await; + client_arc.request("disconnect", empty_params()).await; + // 显式 abort reader + kill+wait child,确保返回前子进程已退出。 + // 即便仍有 in-flight Arc clone(并发 query 未返回),它们会因 reader 关闭 + // 而立即收到 disconnected 错误,然后 Arc 自然 drop。 + client_arc.shutdown().await; } Ok(()) } diff --git a/crates/db/src/ipc/plugin.rs b/crates/db/src/ipc/plugin.rs index 6c5284f3c9..2dc70ac517 100644 --- a/crates/db/src/ipc/plugin.rs +++ b/crates/db/src/ipc/plugin.rs @@ -8,7 +8,7 @@ use crate::ipc::connection::ExternalDbConnection; use crate::ipc::protocol::{database_metadata_params, table_metadata_params}; use crate::ipc::registry::{EXTERNAL_DRIVER_ID_PARAM, IpcDriverManifest, IpcDriverRegistry}; use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; -use crate::plugin_manifest::DatabaseUiManifest; +use crate::plugin_manifest::{DatabaseCapabilities, DatabaseUiCapabilities, DatabaseUiManifest}; use crate::types::*; use anyhow::{Result, anyhow}; use async_trait::async_trait; @@ -57,6 +57,22 @@ impl ExternalDatabasePlugin { )), } } + + async fn optional_metadata( + &self, + connection: &dyn DbConnection, + method: &str, + params: serde_json::Value, + ) -> Result> + where + T: serde::de::DeserializeOwned, + { + match self.metadata(connection, method, params).await { + Ok(value) => Ok(Some(value)), + Err(error) if is_not_supported(&error) => Ok(None), + Err(error) => Err(error), + } + } } impl Default for ExternalDatabasePlugin { @@ -120,29 +136,20 @@ impl DatabasePlugin for ExternalDatabasePlugin { .await { Ok(databases) => Ok(databases), - Err(_) => Ok(names_to_databases(self.list_databases(connection).await?)), + Err(error) if is_not_supported(&error) => { + Ok(names_to_databases(self.list_databases(connection).await?)) + } + Err(error) => Err(error), } } - fn supports_schema(&self) -> bool { - self.registry - .drivers() - .iter() - .any(|driver| driver.dialect.supports_schema) - } - - fn uses_schema_as_database(&self) -> bool { - self.registry - .drivers() - .iter() - .any(|driver| driver.dialect.uses_schema_as_database) - } - - fn supports_sequences(&self) -> bool { - self.registry - .drivers() - .iter() - .any(|driver| driver.dialect.supports_sequences) + fn capabilities(&self) -> DatabaseCapabilities { + merge_capabilities( + self.registry + .drivers() + .iter() + .map(IpcDriverManifest::effective_capabilities), + ) } fn sql_dialect(&self) -> Box { @@ -301,20 +308,89 @@ impl DatabasePlugin for ExternalDatabasePlugin { )) } + async fn list_foreign_keys( + &self, + connection: &dyn DbConnection, + database: &str, + schema: Option, + table: &str, + ) -> Result> { + Ok(self + .optional_metadata( + connection, + "metadata.list_foreign_keys", + table_metadata_params(database, schema, table), + ) + .await? + .unwrap_or_default()) + } + + async fn list_table_triggers( + &self, + connection: &dyn DbConnection, + database: &str, + schema: Option, + table: &str, + ) -> Result> { + Ok(self + .optional_metadata( + connection, + "metadata.list_table_triggers", + table_metadata_params(database, schema, table), + ) + .await? + .unwrap_or_default()) + } + + async fn list_table_checks( + &self, + connection: &dyn DbConnection, + database: &str, + schema: Option, + table: &str, + ) -> Result> { + Ok(self + .optional_metadata( + connection, + "metadata.list_table_checks", + table_metadata_params(database, schema, table), + ) + .await? + .unwrap_or_default()) + } + async fn list_functions( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result> { - Ok(Vec::new()) + Ok(self + .optional_metadata( + connection, + "metadata.list_functions", + database_metadata_params(database, None), + ) + .await? + .unwrap_or_default()) } async fn list_functions_view( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result { - Ok(ObjectView::default()) + let rows = self + .list_functions(connection, database) + .await? + .into_iter() + .map(|function| vec![function.name, function.return_type.unwrap_or_default()]) + .collect(); + Ok(object_view( + DbNodeType::Function, + "Functions", + vec!["Name", "Return Type"], + rows, + )) } fn ui_manifest(&self) -> DatabaseUiManifest { @@ -327,51 +403,110 @@ impl DatabasePlugin for ExternalDatabasePlugin { async fn list_procedures( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result> { - Ok(Vec::new()) + Ok(self + .optional_metadata( + connection, + "metadata.list_procedures", + database_metadata_params(database, None), + ) + .await? + .unwrap_or_default()) } async fn list_procedures_view( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result { - Ok(ObjectView::default()) + let rows = self + .list_procedures(connection, database) + .await? + .into_iter() + .map(|procedure| vec![procedure.name, procedure.parameters.join(", ")]) + .collect(); + Ok(object_view( + DbNodeType::Procedure, + "Procedures", + vec!["Name", "Parameters"], + rows, + )) } async fn list_triggers( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result> { - Ok(Vec::new()) + Ok(self + .optional_metadata( + connection, + "metadata.list_triggers", + database_metadata_params(database, None), + ) + .await? + .unwrap_or_default()) } async fn list_triggers_view( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result { - Ok(ObjectView::default()) + let rows = self + .list_triggers(connection, database) + .await? + .into_iter() + .map(|trigger| vec![trigger.name, trigger.table_name, trigger.event]) + .collect(); + Ok(object_view( + DbNodeType::Trigger, + "Triggers", + vec!["Name", "Table", "Event"], + rows, + )) } async fn list_sequences( &self, - _connection: &dyn DbConnection, - _database: &str, - _schema: Option, + connection: &dyn DbConnection, + database: &str, + schema: Option, ) -> Result> { - Ok(Vec::new()) + Ok(self + .optional_metadata( + connection, + "metadata.list_sequences", + database_metadata_params(database, schema), + ) + .await? + .unwrap_or_default()) } async fn list_sequences_view( &self, - _connection: &dyn DbConnection, - _database: &str, + connection: &dyn DbConnection, + database: &str, ) -> Result { - Ok(ObjectView::default()) + let rows = self + .list_sequences(connection, database, None) + .await? + .into_iter() + .map(|sequence| { + vec![ + sequence.name, + sequence.increment.unwrap_or_default().to_string(), + ] + }) + .collect(); + Ok(object_view( + DbNodeType::Sequence, + "Sequences", + vec!["Name", "Increment"], + rows, + )) } fn build_column_definition(&self, column: &ColumnInfo, include_name: bool) -> String { @@ -516,6 +651,42 @@ fn names_to_databases(names: Vec) -> Vec { .collect() } +fn is_not_supported(error: &anyhow::Error) -> bool { + error + .downcast_ref::() + .is_some_and(|error| matches!(error, DbError::NotSupported(_))) +} + +fn merge_capabilities( + capabilities: impl IntoIterator, +) -> DatabaseCapabilities { + capabilities + .into_iter() + .fold(DatabaseUiCapabilities::default(), |mut merged, current| { + merged.supports_schema |= current.supports_schema; + merged.uses_schema_as_database |= current.uses_schema_as_database; + merged.supports_sequences |= current.supports_sequences; + merged.supports_functions |= current.supports_functions; + merged.supports_procedures |= current.supports_procedures; + merged.supports_triggers |= current.supports_triggers; + merged.supports_table_engine |= current.supports_table_engine; + merged.supports_table_charset |= current.supports_table_charset; + merged.supports_table_collation |= current.supports_table_collation; + merged.supports_auto_increment |= current.supports_auto_increment; + merged.supports_tablespace |= current.supports_tablespace; + merged.supports_unsigned |= current.supports_unsigned; + merged.supports_enum_values |= current.supports_enum_values; + merged.show_charset_in_column_detail |= current.show_charset_in_column_detail; + merged.show_collation_in_column_detail |= current.show_collation_in_column_detail; + for engine in current.table_engines { + if !merged.table_engines.contains(&engine) { + merged.table_engines.push(engine); + } + } + merged + }) +} + fn object_view( db_node_type: DbNodeType, title: impl Into, diff --git a/crates/db/src/ipc/registry.rs b/crates/db/src/ipc/registry.rs index 7ebf800c1c..db08d659b5 100644 --- a/crates/db/src/ipc/registry.rs +++ b/crates/db/src/ipc/registry.rs @@ -1,5 +1,5 @@ use crate::connection::DbError; -use crate::plugin_manifest::DatabaseUiManifest; +use crate::plugin_manifest::{DatabaseCapabilities, DatabaseUiManifest}; use one_core::storage::get_config_dir; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -20,6 +20,8 @@ pub struct IpcDriverManifest { #[serde(default)] pub dialect: IpcDriverDialect, #[serde(default)] + pub capabilities: Option, + #[serde(default)] pub ui: IpcDriverUi, #[serde(skip)] pub manifest_dir: PathBuf, @@ -103,6 +105,23 @@ impl IpcDriverManifest { .unwrap_or_else(|| self.manifest_dir.clone()) } + pub fn effective_capabilities(&self) -> DatabaseCapabilities { + let mut capabilities = self + .ui + .form + .as_ref() + .map(|manifest| manifest.capabilities.clone()) + .unwrap_or_else(|| DatabaseCapabilities { + supports_functions: true, + supports_procedures: true, + ..DatabaseCapabilities::default() + }); + capabilities.supports_schema |= self.dialect.supports_schema; + capabilities.supports_sequences |= self.dialect.supports_sequences; + capabilities.uses_schema_as_database |= self.dialect.uses_schema_as_database; + self.capabilities.clone().unwrap_or(capabilities) + } + fn validate(&self) -> Result<(), DbError> { if self.id.trim().is_empty() || self.name.trim().is_empty() { return Err(DbError::connection( @@ -244,4 +263,40 @@ mod tests { assert_eq!(registry.drivers().len(), 1); assert_eq!(registry.find("demo").unwrap().name, "Demo"); } + + #[test] + fn parses_top_level_capabilities() { + let manifest: IpcDriverManifest = serde_json::from_str( + r#"{"id":"demo","name":"Demo","entry":{"command":"python3"},"transport":{"name":"demo.sock"},"dialect":{"supports_schema":false},"capabilities":{"supports_schema":true,"supports_functions":true}}"#, + ) + .unwrap(); + + let capabilities = manifest.effective_capabilities(); + assert!(capabilities.supports_schema); + assert!(capabilities.supports_functions); + } + + #[test] + fn falls_back_to_legacy_dialect_capabilities() { + let manifest: IpcDriverManifest = serde_json::from_str( + r#"{"id":"demo","name":"Demo","entry":{"command":"python3"},"transport":{"name":"demo.sock"},"dialect":{"supports_schema":true,"supports_sequences":true}}"#, + ) + .unwrap(); + + let capabilities = manifest.effective_capabilities(); + assert!(capabilities.supports_schema); + assert!(capabilities.supports_sequences); + assert!(capabilities.supports_functions); + assert!(capabilities.supports_procedures); + } + + #[test] + fn falls_back_to_legacy_ui_form_capabilities() { + let manifest: IpcDriverManifest = serde_json::from_str( + r#"{"id":"demo","name":"Demo","entry":{"command":"python3"},"transport":{"name":"demo.sock"},"ui":{"form":{"schema_version":1,"capabilities":{"supports_triggers":true},"forms":[],"actions":{"actions":[]}}}}"#, + ) + .unwrap(); + + assert!(manifest.effective_capabilities().supports_triggers); + } } diff --git a/crates/db/src/manager.rs b/crates/db/src/manager.rs index 9b613d71f0..e958878b73 100644 --- a/crates/db/src/manager.rs +++ b/crates/db/src/manager.rs @@ -11,6 +11,7 @@ use crate::mssql::MsSqlPlugin; use crate::mysql::MySqlPlugin; use crate::oracle::OraclePlugin; use crate::plugin::DatabasePlugin; +use crate::plugin_manifest::DatabaseCapabilities; use crate::postgresql::PostgresPlugin; use crate::sqlite::SqlitePlugin; use crate::{ @@ -731,8 +732,7 @@ impl ConnectionPool { _db_manager: &DbManager, ) -> anyhow::Result>>> { let plugin = self.db_manager.get_plugin(&config.database_type)?; - let mut connection = plugin.create_connection(config).await?; - connection.connect().await?; + let connection = plugin.create_connection(config).await?; Ok(Arc::new(RwLock::new(connection))) } } @@ -1550,20 +1550,11 @@ impl GlobalDbState { }) } - /// Check if database type supports schemas - pub fn supports_schema(&self, database_type: &DatabaseType) -> bool { + pub fn capabilities(&self, database_type: &DatabaseType) -> DatabaseCapabilities { self.db_manager .get_plugin(database_type) - .map(|plugin| plugin.supports_schema()) - .unwrap_or(false) - } - - /// Check if database type uses schemas as top-level nodes (like Oracle) - pub fn uses_schema_as_database(&self, database_type: &DatabaseType) -> bool { - self.db_manager - .get_plugin(database_type) - .map(|plugin| plugin.uses_schema_as_database()) - .unwrap_or(false) + .map(|plugin| plugin.capabilities()) + .unwrap_or_default() } /// List schemas in a database (with caching) @@ -1954,6 +1945,14 @@ impl GlobalDbState { connection_id: String, node: DbNode, ) -> anyhow::Result> { + if node.node_type == DbNodeType::Connection && !node.children_loaded { + info!( + "[DB][Timing] load_object_view skipped connection_id={} node_id={} reason=connection_children_not_loaded", + connection_id, node.id + ); + return Ok(None); + } + let mut config = self .get_config(&connection_id) .ok_or_else(|| anyhow::anyhow!("Connection not found: {}", connection_id))? @@ -1986,7 +1985,7 @@ impl GlobalDbState { let view = match node.node_type { DbNodeType::Connection => { if node.children_loaded { - if plugin.uses_schema_as_database() { + if plugin.capabilities().uses_schema_as_database { plugin.list_schemas_view(&*conn, &database).await.ok() } else { plugin.list_databases_view(&*conn).await.ok() @@ -1996,7 +1995,7 @@ impl GlobalDbState { } } DbNodeType::Database => { - if plugin.supports_schema() { + if plugin.capabilities().supports_schema { plugin.list_schemas_view(&*conn, &database).await.ok() } else { plugin.list_tables_view(&*conn, &database, None).await.ok() diff --git a/crates/db/src/mssql/plugin.rs b/crates/db/src/mssql/plugin.rs index d4218ccfa7..a2b97c3a43 100644 --- a/crates/db/src/mssql/plugin.rs +++ b/crates/db/src/mssql/plugin.rs @@ -21,8 +21,8 @@ use crate::mssql::connection::MssqlDbConnection; use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiCapabilities, - DatabaseUiManifest, FormSelectOption, ReferenceDataKind, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, FormSelectOption, ReferenceDataKind, }; use crate::types::*; @@ -588,6 +588,18 @@ impl DatabasePlugin for MsSqlPlugin { format!("[{}]", identifier.replace("]", "]]")) } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_schema: true, + supports_sequences: true, + supports_functions: true, + supports_procedures: true, + supports_triggers: true, + supports_table_collation: true, + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { MSSQL_UI_MANIFEST.clone() } @@ -885,14 +897,6 @@ impl DatabasePlugin for MsSqlPlugin { } } - fn supports_schema(&self) -> bool { - true - } - - fn supports_sequences(&self) -> bool { - true - } - fn sql_dialect(&self) -> Box { Box::new(sqlparser::dialect::MsSqlDialect {}) } @@ -2555,15 +2559,15 @@ mod tests { } #[test] - fn test_supports_schema() { + fn test_capabilities_support_schema() { let plugin = create_plugin(); - assert!(plugin.supports_schema()); + assert!(plugin.capabilities().supports_schema); } #[test] - fn test_supports_sequences() { + fn test_capabilities_support_sequences() { let plugin = create_plugin(); - assert!(plugin.supports_sequences()); + assert!(plugin.capabilities().supports_sequences); } #[test] diff --git a/crates/db/src/mysql/plugin.rs b/crates/db/src/mysql/plugin.rs index 9c4fed62ef..c9f58f5a44 100644 --- a/crates/db/src/mysql/plugin.rs +++ b/crates/db/src/mysql/plugin.rs @@ -15,10 +15,10 @@ use crate::mysql::connection::MysqlDbConnection; use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionDescriptor, DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, - DatabaseActionTarget, DatabaseActionToolbarScope, DatabaseFormField, DatabaseFormFieldType, - DatabaseFormKind, DatabaseFormManifest, DatabaseFormTab, DatabaseUiCapabilities, - DatabaseUiManifest, FormDefaultRule, FormSelectOption, FormValueCondition, FormVisibilityRule, - ReferenceDataKind, + DatabaseActionTarget, DatabaseActionToolbarScope, DatabaseCapabilities, DatabaseFormField, + DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseFormTab, + DatabaseUiCapabilities, DatabaseUiManifest, FormDefaultRule, FormSelectOption, + FormValueCondition, FormVisibilityRule, ReferenceDataKind, }; use crate::types::*; @@ -1004,6 +1004,24 @@ impl DatabasePlugin for MySqlPlugin { }.with_standard_sql() } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_functions: true, + supports_procedures: true, + supports_triggers: true, + supports_table_engine: true, + supports_table_charset: true, + supports_table_collation: true, + supports_auto_increment: true, + supports_unsigned: true, + supports_enum_values: true, + show_charset_in_column_detail: true, + show_collation_in_column_detail: true, + table_engines: self.engines(), + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { MYSQL_UI_MANIFEST.clone() } diff --git a/crates/db/src/oracle/plugin.rs b/crates/db/src/oracle/plugin.rs index eb16d94334..c5f340f8cb 100644 --- a/crates/db/src/oracle/plugin.rs +++ b/crates/db/src/oracle/plugin.rs @@ -22,8 +22,8 @@ use crate::oracle::connection::OracleDbConnection; use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiCapabilities, - DatabaseUiManifest, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, }; use crate::types::*; @@ -485,6 +485,18 @@ impl DatabasePlugin for OraclePlugin { format!("\"{}\"", identifier.replace("\"", "\"\"")) } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + uses_schema_as_database: true, + supports_sequences: true, + supports_functions: true, + supports_procedures: true, + supports_triggers: true, + supports_tablespace: true, + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { ORACLE_UI_MANIFEST.clone() } @@ -493,10 +505,6 @@ impl DatabasePlugin for OraclePlugin { Box::new(sqlparser::dialect::OracleDialect {}) } - fn supports_sequences(&self) -> bool { - true - } - fn supports_rowid(&self) -> bool { true } @@ -581,10 +589,6 @@ impl DatabasePlugin for OraclePlugin { }) } - fn uses_schema_as_database(&self) -> bool { - true - } - async fn list_schemas( &self, connection: &dyn DbConnection, @@ -2146,15 +2150,15 @@ mod tests { } #[test] - fn test_supports_sequences() { + fn test_capabilities_support_sequences() { let plugin = create_plugin(); - assert!(plugin.supports_sequences()); + assert!(plugin.capabilities().supports_sequences); } #[test] - fn test_supports_schema() { + fn test_capabilities_do_not_support_schema() { let plugin = create_plugin(); - assert!(!plugin.supports_schema()); + assert!(!plugin.capabilities().supports_schema); } #[test] diff --git a/crates/db/src/plugin.rs b/crates/db/src/plugin.rs index 7ae220ea83..913cae770d 100644 --- a/crates/db/src/plugin.rs +++ b/crates/db/src/plugin.rs @@ -9,7 +9,8 @@ use crate::import_export::{ }, }; use crate::plugin_manifest::{ - DatabaseUiCapabilities, DatabaseUiManifest, FormSelectOption, ReferenceDataKind, + DatabaseCapabilities, DatabaseUiCapabilities, DatabaseUiManifest, FormSelectOption, + ReferenceDataKind, }; use crate::streaming_parser::StreamingSqlParser; use crate::types::*; @@ -142,22 +143,6 @@ pub trait DatabasePlugin: Send + Sync { connection: &dyn DbConnection, ) -> Result>; - /// Whether this database supports schemas (e.g., PostgreSQL, MSSQL) - fn supports_schema(&self) -> bool { - false - } - - /// Whether this database uses schemas as top-level nodes instead of databases. - /// Oracle uses this because it connects via service_name and then lists schemas (users). - fn uses_schema_as_database(&self) -> bool { - false - } - - /// Whether this database supports sequences (e.g., PostgreSQL, Oracle, MSSQL) - fn supports_sequences(&self) -> bool { - false - } - /// Whether this database supports rowid for row identification (e.g., Oracle, SQLite) fn supports_rowid(&self) -> bool { false @@ -425,10 +410,6 @@ pub trait DatabasePlugin: Send + Sync { // === Function Operations === - fn supports_functions(&self) -> bool { - true - } - async fn list_functions( &self, connection: &dyn DbConnection, @@ -441,23 +422,17 @@ pub trait DatabasePlugin: Send + Sync { database: &str, ) -> Result; - fn supports_procedures(&self) -> bool { - true + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_functions: true, + supports_procedures: true, + table_engines: self.engines(), + ..DatabaseUiCapabilities::default() + } } fn ui_manifest(&self) -> DatabaseUiManifest { - DatabaseUiManifest { - capabilities: DatabaseUiCapabilities { - supports_schema: self.supports_schema(), - uses_schema_as_database: self.uses_schema_as_database(), - supports_sequences: self.supports_sequences(), - supports_functions: self.supports_functions(), - supports_procedures: self.supports_procedures(), - table_engines: self.engines(), - ..DatabaseUiCapabilities::default() - }, - ..DatabaseUiManifest::default() - } + DatabaseUiManifest::default() } fn resolve_reference_data( @@ -569,7 +544,7 @@ pub trait DatabasePlugin: Send + Sync { let id = &node.id; let schemas; let mut metadata: HashMap = HashMap::new(); - if self.uses_schema_as_database() { + if self.capabilities().uses_schema_as_database { schemas = self.list_schemas(connection, "").await?; metadata.insert("database".to_string(), "".to_string()); } else { @@ -693,8 +668,10 @@ pub trait DatabasePlugin: Send + Sync { } nodes.push(views_folder); + let capabilities = self.capabilities(); + // Functions folder - if self.supports_functions() { + if capabilities.supports_functions { let functions = self .list_functions(connection, database) .await @@ -730,7 +707,7 @@ pub trait DatabasePlugin: Send + Sync { } // Procedures folder - if self.supports_procedures() { + if capabilities.supports_procedures { let procedures = self .list_procedures(connection, database) .await @@ -766,7 +743,7 @@ pub trait DatabasePlugin: Send + Sync { } // Sequences folder (only for databases that support sequences) - if self.supports_sequences() { + if capabilities.supports_sequences { let sequences = self .list_sequences(connection, database, schema) .await @@ -848,14 +825,14 @@ pub trait DatabasePlugin: Send + Sync { let id = &node.id; match node.node_type { DbNodeType::Connection => { - if self.uses_schema_as_database() { + if self.capabilities().uses_schema_as_database { self.build_schema_tree(connection, node).await } else { self.build_database_tree(connection, node).await } } DbNodeType::Database => { - if self.supports_schema() { + if self.capabilities().supports_schema { self.build_schema_tree(connection, node).await } else { self.build_database_or_schema_children(connection, node, None) @@ -2814,6 +2791,16 @@ mod tests { use sqlparser::dialect::MySqlDialect; use sqlparser::parser::Parser; + // ==================== capabilities tests ==================== + + #[test] + fn default_capabilities_support_functions_and_procedures() { + let plugin = MySqlPlugin::new(); + let capabilities = DatabasePlugin::capabilities(&plugin); + assert!(capabilities.supports_functions); + assert!(capabilities.supports_procedures); + } + // ==================== is_query_stmt tests (AST-based) ==================== #[test] diff --git a/crates/db/src/plugin_manifest.rs b/crates/db/src/plugin_manifest.rs index cc64549b30..620badfc61 100644 --- a/crates/db/src/plugin_manifest.rs +++ b/crates/db/src/plugin_manifest.rs @@ -24,6 +24,7 @@ impl Default for DatabaseUiManifest { } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] pub struct DatabaseUiCapabilities { pub supports_schema: bool, pub uses_schema_as_database: bool, @@ -43,6 +44,8 @@ pub struct DatabaseUiCapabilities { pub table_engines: Vec, } +pub type DatabaseCapabilities = DatabaseUiCapabilities; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum DatabaseFormKind { Connection, diff --git a/crates/db/src/postgresql/plugin.rs b/crates/db/src/postgresql/plugin.rs index 0c6c90a376..ab75a66cdd 100644 --- a/crates/db/src/postgresql/plugin.rs +++ b/crates/db/src/postgresql/plugin.rs @@ -20,8 +20,8 @@ use crate::manifest_helpers::{ use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiCapabilities, - DatabaseUiManifest, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, }; use crate::postgresql::connection::PostgresDbConnection; use crate::types::*; @@ -681,6 +681,20 @@ impl DatabasePlugin for PostgresPlugin { format!("\"{}\"", identifier.replace("\"", "\"\"")) } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_schema: true, + supports_sequences: true, + supports_functions: true, + supports_procedures: true, + supports_triggers: true, + supports_table_charset: true, + supports_table_collation: true, + supports_tablespace: true, + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { POSTGRESQL_UI_MANIFEST.clone() } @@ -997,14 +1011,6 @@ impl DatabasePlugin for PostgresPlugin { } } - fn supports_schema(&self) -> bool { - true - } - - fn supports_sequences(&self) -> bool { - true - } - fn sql_dialect(&self) -> Box { Box::new(sqlparser::dialect::PostgreSqlDialect {}) } @@ -2226,15 +2232,15 @@ mod tests { } #[test] - fn test_supports_schema() { + fn test_capabilities_support_schema() { let plugin = create_plugin(); - assert!(plugin.supports_schema()); + assert!(plugin.capabilities().supports_schema); } #[test] - fn test_supports_sequences() { + fn test_capabilities_support_sequences() { let plugin = create_plugin(); - assert!(plugin.supports_sequences()); + assert!(plugin.capabilities().supports_sequences); } #[test] diff --git a/crates/db/src/sqlite/plugin.rs b/crates/db/src/sqlite/plugin.rs index 8cdbac90fd..00b62dfbdb 100644 --- a/crates/db/src/sqlite/plugin.rs +++ b/crates/db/src/sqlite/plugin.rs @@ -15,8 +15,8 @@ use crate::manifest_helpers::{DatabaseActionDescriptorExt, action, action_with_s use crate::plugin::{DatabasePlugin, SqlCompletionInfo}; use crate::plugin_manifest::{ DatabaseActionId, DatabaseActionManifest, DatabaseActionPlacement, DatabaseActionToolbarScope, - DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, DatabaseUiCapabilities, - DatabaseUiManifest, + DatabaseCapabilities, DatabaseFormFieldType, DatabaseFormKind, DatabaseFormManifest, + DatabaseUiCapabilities, DatabaseUiManifest, }; use crate::sqlite::SqliteDbConnection; use crate::types::*; @@ -572,6 +572,13 @@ impl DatabasePlugin for SqlitePlugin { DatabaseType::SQLite } + fn capabilities(&self) -> DatabaseCapabilities { + DatabaseUiCapabilities { + supports_auto_increment: true, + ..DatabaseUiCapabilities::default() + } + } + fn ui_manifest(&self) -> DatabaseUiManifest { SQLITE_UI_MANIFEST.clone() } @@ -1013,10 +1020,6 @@ impl DatabasePlugin for SqlitePlugin { }) } - fn supports_functions(&self) -> bool { - false - } - async fn list_functions( &self, _connection: &dyn DbConnection, @@ -1042,10 +1045,6 @@ impl DatabasePlugin for SqlitePlugin { }) } - fn supports_procedures(&self) -> bool { - false - } - async fn list_procedures( &self, _connection: &dyn DbConnection, @@ -1460,6 +1459,15 @@ mod tests { assert_eq!(plugin.quote_identifier("col\"umn"), "\"col\"\"umn\""); } + #[test] + fn test_capabilities() { + let capabilities = create_plugin().capabilities(); + assert!(capabilities.supports_auto_increment); + assert!(!capabilities.supports_functions); + assert!(!capabilities.supports_procedures); + assert!(!capabilities.supports_sequences); + } + #[test] fn test_ui_manifest_smoke() { let manifest = create_plugin().ui_manifest(); diff --git a/crates/db/tests/ipc_concurrency.rs b/crates/db/tests/ipc_concurrency.rs index fdce037d3a..7ec9814bc8 100644 --- a/crates/db/tests/ipc_concurrency.rs +++ b/crates/db/tests/ipc_concurrency.rs @@ -39,6 +39,7 @@ fn make_manifest(socket_name: String) -> IpcDriverManifest { working_dir: None, }, dialect: Default::default(), + capabilities: None, ui: Default::default(), transport: IpcDriverTransport::local_socket(socket_name), manifest_dir: PathBuf::new(), diff --git a/crates/db/tests/ipc_duckdb_driver.rs b/crates/db/tests/ipc_duckdb_driver.rs index 314fbe21af..57aff32eb2 100644 --- a/crates/db/tests/ipc_duckdb_driver.rs +++ b/crates/db/tests/ipc_duckdb_driver.rs @@ -26,40 +26,28 @@ fn driver_binary() -> PathBuf { target_dir.join(name) } -#[tokio::test] -async fn duckdb_driver_ipc_full_integration() { - let binary = driver_binary(); - if !binary.exists() { - eprintln!( - "SKIP: duckdb_driver binary not found at {:?}\n\ - Build it first: cargo build -p duckdb_driver", - binary - ); - return; - } - - let temp = tempfile::tempdir().unwrap(); - let socket = format!("onetcli-test-duckdb-{}.sock", uuid::Uuid::new_v4()); - let db_path = temp.path().join("test.db"); - - let driver = IpcDriverManifest { +fn make_driver(binary: &std::path::Path, manifest_dir: &std::path::Path) -> IpcDriverManifest { + IpcDriverManifest { id: "duckdb".into(), name: "DuckDB".into(), description: String::new(), version: String::new(), entry: IpcDriverEntry { command: binary.to_string_lossy().into_owned(), - args: vec![socket.clone()], + args: Vec::new(), working_dir: None, }, dialect: Default::default(), + capabilities: None, ui: Default::default(), - transport: IpcDriverTransport::local_socket(socket), - manifest_dir: temp.path().to_path_buf(), - }; + transport: IpcDriverTransport::local_socket("duckdb-driver.sock"), + manifest_dir: manifest_dir.to_path_buf(), + } +} - let config = DbConnectionConfig { - id: "duckdb-test".into(), +fn make_config(id: &str, db_path: &std::path::Path) -> DbConnectionConfig { + DbConnectionConfig { + id: id.into(), name: "DuckDB Test".into(), database_type: DatabaseType::External, host: db_path.to_string_lossy().into_owned(), @@ -71,7 +59,32 @@ async fn duckdb_driver_ipc_full_integration() { sid: None, workspace_id: None, extra_params: HashMap::new(), - }; + } +} + +fn skip_if_missing_binary(binary: &std::path::Path) -> bool { + if binary.exists() { + return false; + } + eprintln!( + "SKIP: duckdb_driver binary not found at {:?}\n\ + Build it first: cargo build -p duckdb_driver", + binary + ); + true +} + +#[tokio::test] +async fn duckdb_driver_ipc_full_integration() { + let binary = driver_binary(); + if skip_if_missing_binary(&binary) { + return; + } + + let temp = tempfile::tempdir().unwrap(); + let db_path = temp.path().join("test.db"); + let driver = make_driver(&binary, temp.path()); + let config = make_config("duckdb-test", &db_path); // ---- connect ---- let mut conn = ExternalDbConnection::new(config.clone(), driver); @@ -149,3 +162,37 @@ async fn duckdb_driver_ipc_full_integration() { // ---- disconnect ---- conn.disconnect().await.expect("disconnect"); } + +#[tokio::test] +async fn same_duckdb_driver_can_open_multiple_connections_concurrently() { + let binary = driver_binary(); + if skip_if_missing_binary(&binary) { + return; + } + + let temp = tempfile::tempdir().unwrap(); + let mut handles = Vec::new(); + for idx in 0..3 { + let driver = make_driver(&binary, temp.path()); + let db_path = temp.path().join(format!("multi-{idx}.db")); + let config = make_config(&format!("duckdb-multi-{idx}"), &db_path); + handles.push(tokio::spawn(async move { + let mut conn = ExternalDbConnection::new(config, driver); + conn.connect().await.expect("connect"); + let result = conn.query(&format!("SELECT {idx} AS val")).await.unwrap(); + match result { + SqlResult::Query(q) => { + let expected = idx.to_string(); + assert_eq!(q.columns, vec!["val"]); + assert_eq!(q.rows[0][0].as_deref(), Some(expected.as_str())); + } + other => panic!("expected query result, got {other:?}"), + } + conn.disconnect().await.expect("disconnect"); + })); + } + + for handle in handles { + handle.await.unwrap(); + } +} diff --git a/crates/db/tests/ipc_mock_driver.rs b/crates/db/tests/ipc_mock_driver.rs index 83bac13b18..9b81779df2 100644 --- a/crates/db/tests/ipc_mock_driver.rs +++ b/crates/db/tests/ipc_mock_driver.rs @@ -36,6 +36,7 @@ async fn external_connection_uses_mock_local_socket_driver() { working_dir: None, }, dialect: Default::default(), + capabilities: None, ui: Default::default(), transport: IpcDriverTransport::local_socket(socket_name), manifest_dir: PathBuf::new(), diff --git a/crates/db_view/src/chatdb/chat_panel.rs b/crates/db_view/src/chatdb/chat_panel.rs index ac20adb942..8b77c4d6ce 100644 --- a/crates/db_view/src/chatdb/chat_panel.rs +++ b/crates/db_view/src/chatdb/chat_panel.rs @@ -346,6 +346,9 @@ impl ChatPanel { self.chat_history.clear(); self.sql_result_views.clear(); self.sql_block_results.clear(); + self.latest_ai_message_id = None; + self.render_limit = MESSAGE_RENDER_LIMIT; + self.session_affinity.reset(); cx.notify(); } diff --git a/crates/db_view/src/chatdb/db_connection_selector.rs b/crates/db_view/src/chatdb/db_connection_selector.rs index 66242ed202..a2d6e36f52 100644 --- a/crates/db_view/src/chatdb/db_connection_selector.rs +++ b/crates/db_view/src/chatdb/db_connection_selector.rs @@ -223,14 +223,6 @@ impl DbConnectionSelector { )) } - pub fn supports_schema(&self) -> bool { - self.supports_schema - } - - pub fn uses_schema_as_database(&self) -> bool { - self.uses_schema_as_database - } - fn snapshot(&self) -> DbConnectionSelectorSnapshot { DbConnectionSelectorSnapshot { connections: self.connections.clone(), @@ -427,9 +419,9 @@ impl DbConnectionSelector { self.loading_schemas = false; let global_db_state = cx.global::().clone(); - self.supports_schema = global_db_state.supports_schema(&connection.database_type); - self.uses_schema_as_database = - global_db_state.uses_schema_as_database(&connection.database_type); + let capabilities = global_db_state.capabilities(&connection.database_type); + self.supports_schema = capabilities.supports_schema; + self.uses_schema_as_database = capabilities.uses_schema_as_database; self.register_connection(connection.id.clone(), cx); self.emit_selection(cx); diff --git a/crates/db_view/src/database_objects_tab.rs b/crates/db_view/src/database_objects_tab.rs index 7315033a07..fac8de84f7 100644 --- a/crates/db_view/src/database_objects_tab.rs +++ b/crates/db_view/src/database_objects_tab.rs @@ -899,7 +899,7 @@ impl DatabaseObjects { }); let toolbar_buttons = - build_toolbar_buttons_for(database_type, node_type, data_db_node_type); + build_toolbar_buttons_for(database_type, node_type, data_db_node_type, cx); for btn_config in toolbar_buttons { let button = match btn_config.button_type { diff --git a/crates/db_view/src/database_view_plugin.rs b/crates/db_view/src/database_view_plugin.rs index caa167a8e2..53ca970517 100644 --- a/crates/db_view/src/database_view_plugin.rs +++ b/crates/db_view/src/database_view_plugin.rs @@ -1,18 +1,10 @@ use db::DbNodeType; -use db::clickhouse::ClickHousePlugin; -use db::duckdb::DuckDbPlugin; -use db::ipc::ExternalDatabasePlugin; use db::ipc::{EXTERNAL_DRIVER_ID_PARAM, IpcDriverManifest, IpcDriverRegistry}; -use db::mssql::MsSqlPlugin; -use db::mysql::MySqlPlugin; -use db::oracle::OraclePlugin; use db::plugin::DatabasePlugin; use db::plugin_manifest::{ DatabaseActionDescriptor, DatabaseActionId, DatabaseActionPlacement, - DatabaseActionToolbarScope, DatabaseFormKind, DatabaseUiManifest, + DatabaseActionToolbarScope, DatabaseCapabilities, DatabaseFormKind, DatabaseUiManifest, }; -use db::postgresql::PostgresPlugin; -use db::sqlite::SqlitePlugin; use gpui::{App, AppContext, Entity, Window}; use gpui_component::IconName; use one_core::storage::DatabaseType; @@ -201,13 +193,15 @@ impl Default for ColumnEditorCapabilities { struct ManifestDatabaseViewPlugin { database_type: DatabaseType, manifest: DatabaseUiManifest, + capabilities: DatabaseCapabilities, } impl ManifestDatabaseViewPlugin { - fn new(database_type: DatabaseType) -> Self { + fn new(database_type: DatabaseType, plugin: &dyn DatabasePlugin) -> Self { Self { database_type, - manifest: build_ui_manifest(database_type), + manifest: plugin.ui_manifest(), + capabilities: plugin.capabilities(), } } @@ -304,15 +298,15 @@ impl ManifestDatabaseViewPlugin { } fn get_table_designer_capabilities(&self) -> TableDesignerCapabilities { - to_table_designer_capabilities(&self.manifest.capabilities) + to_table_designer_capabilities(&self.capabilities) } fn get_engines(&self) -> Vec { - self.manifest.capabilities.table_engines.clone() + self.capabilities.table_engines.clone() } fn get_column_editor_capabilities(&self) -> ColumnEditorCapabilities { - to_column_editor_capabilities(&self.manifest.capabilities) + to_column_editor_capabilities(&self.capabilities) } fn build_context_menu(&self, node_id: &str, node_type: DbNodeType) -> Vec { @@ -404,8 +398,16 @@ impl ManifestDatabaseViewPlugin { } } -fn manifest_plugin(database_type: DatabaseType) -> ManifestDatabaseViewPlugin { - ManifestDatabaseViewPlugin::new(database_type) +fn manifest_plugin( + database_type: DatabaseType, + cx: &impl AppContext, +) -> ManifestDatabaseViewPlugin { + let plugin = cx.read_global::(|state, _| { + state + .get_plugin(&database_type) + .expect("database plugin should exist") + }); + ManifestDatabaseViewPlugin::new(database_type, plugin.as_ref()) } fn action_to_context_menu_item( @@ -567,7 +569,7 @@ pub fn create_connection_form_for( window: &mut Window, cx: &mut App, ) -> Entity { - manifest_plugin(database_type).create_connection_form(window, cx) + manifest_plugin(database_type, cx).create_connection_form(window, cx) } pub fn create_external_connection_form_for( @@ -654,7 +656,7 @@ pub fn create_database_editor_view_for_new( window: &mut Window, cx: &mut App, ) -> Entity { - manifest_plugin(database_type).create_database_editor_view(connection_id, window, cx) + manifest_plugin(database_type, cx).create_database_editor_view(connection_id, window, cx) } pub fn create_database_editor_view_for_edit_type( @@ -664,7 +666,7 @@ pub fn create_database_editor_view_for_edit_type( window: &mut Window, cx: &mut App, ) -> Entity { - manifest_plugin(database_type).create_database_editor_view_for_edit( + manifest_plugin(database_type, cx).create_database_editor_view_for_edit( connection_id, database_name, window, @@ -679,7 +681,7 @@ pub fn create_schema_editor_view_for( window: &mut Window, cx: &mut App, ) -> Option> { - manifest_plugin(database_type).create_schema_editor_view( + manifest_plugin(database_type, cx).create_schema_editor_view( connection_id, database_name, window, @@ -691,8 +693,9 @@ pub fn build_context_menu_for( database_type: DatabaseType, node_id: &str, node_type: DbNodeType, + cx: &impl AppContext, ) -> Vec { - let mut items = manifest_plugin(database_type).build_context_menu(node_id, node_type); + let mut items = manifest_plugin(database_type, cx).build_context_menu(node_id, node_type); append_er_diagram_item(&mut items, node_id, node_type); items } @@ -701,8 +704,9 @@ pub fn build_toolbar_buttons_for( database_type: DatabaseType, node_type: DbNodeType, data_node_type: DbNodeType, + cx: &impl AppContext, ) -> Vec { - manifest_plugin(database_type).build_toolbar_buttons(node_type, data_node_type) + manifest_plugin(database_type, cx).build_toolbar_buttons(node_type, data_node_type) } fn append_er_diagram_item(items: &mut Vec, node_id: &str, node_type: DbNodeType) { @@ -722,29 +726,20 @@ fn append_er_diagram_item(items: &mut Vec, node_id: &str, node_ pub fn get_table_designer_capabilities_for( database_type: DatabaseType, + cx: &impl AppContext, ) -> TableDesignerCapabilities { - manifest_plugin(database_type).get_table_designer_capabilities() + manifest_plugin(database_type, cx).get_table_designer_capabilities() } -pub fn get_column_editor_capabilities_for(database_type: DatabaseType) -> ColumnEditorCapabilities { - manifest_plugin(database_type).get_column_editor_capabilities() +pub fn get_column_editor_capabilities_for( + database_type: DatabaseType, + cx: &impl AppContext, +) -> ColumnEditorCapabilities { + manifest_plugin(database_type, cx).get_column_editor_capabilities() } -pub fn get_engines_for(database_type: DatabaseType) -> Vec { - manifest_plugin(database_type).get_engines() -} - -fn build_ui_manifest(database_type: DatabaseType) -> DatabaseUiManifest { - match database_type { - DatabaseType::MySQL => MySqlPlugin::new().ui_manifest(), - DatabaseType::PostgreSQL => PostgresPlugin::new().ui_manifest(), - DatabaseType::MSSQL => MsSqlPlugin::new().ui_manifest(), - DatabaseType::Oracle => OraclePlugin::new().ui_manifest(), - DatabaseType::ClickHouse => ClickHousePlugin::new().ui_manifest(), - DatabaseType::SQLite => SqlitePlugin::new().ui_manifest(), - DatabaseType::DuckDB => DuckDbPlugin::new().ui_manifest(), - DatabaseType::External => ExternalDatabasePlugin::new().ui_manifest(), - } +pub fn get_engines_for(database_type: DatabaseType, cx: &impl AppContext) -> Vec { + manifest_plugin(database_type, cx).get_engines() } fn map_tree_event(action_id: DatabaseActionId, node_id: &str) -> Option { @@ -902,6 +897,12 @@ fn action_id(action: &DatabaseActionDescriptor) -> &'static str { #[cfg(test)] mod tests { use super::*; + use db::mysql::MySqlPlugin; + + fn mysql_manifest_plugin() -> ManifestDatabaseViewPlugin { + let plugin = MySqlPlugin::new(); + ManifestDatabaseViewPlugin::new(DatabaseType::MySQL, &plugin) + } fn has_label(items: &[ContextMenuItem], expected: &str) -> bool { items.iter().any(|item| match item { @@ -915,7 +916,7 @@ mod tests { #[test] fn mysql_table_context_menu_keeps_design_table_action() { - let items = build_context_menu_for(DatabaseType::MySQL, "node-1", DbNodeType::Table); + let items = mysql_manifest_plugin().build_context_menu("node-1", DbNodeType::Table); assert!( has_label(&items, &translate("Table.design_table")), @@ -925,7 +926,7 @@ mod tests { #[test] fn mysql_table_context_menu_keeps_dump_sql_submenu() { - let items = build_context_menu_for(DatabaseType::MySQL, "node-1", DbNodeType::Table); + let items = mysql_manifest_plugin().build_context_menu("node-1", DbNodeType::Table); let dump_submenu = items.iter().find_map(|item| match item { ContextMenuItem::Submenu { label, items, .. } @@ -956,7 +957,7 @@ mod tests { #[test] fn mysql_database_context_menu_restores_legacy_order_and_separators() { - let items = build_context_menu_for(DatabaseType::MySQL, "node-1", DbNodeType::Database); + let items = mysql_manifest_plugin().build_context_menu("node-1", DbNodeType::Database); let labels: Vec = items .iter() diff --git a/crates/db_view/src/db_tree_view.rs b/crates/db_view/src/db_tree_view.rs index d540c82202..5b58fe8529 100644 --- a/crates/db_view/src/db_tree_view.rs +++ b/crates/db_view/src/db_tree_view.rs @@ -2637,7 +2637,7 @@ impl DbTreeView { let is_active = conn_active && (node.node_type != DbNodeType::Database || node.children_loaded); - let menu_items = build_context_menu_for(node.database_type, node_id, node.node_type); + let menu_items = build_context_menu_for(node.database_type, node_id, node.node_type, cx); if !menu_items.is_empty() { // 渲染 plugin 提供的菜单,传入连接激活状态 menu = Self::render_context_menu_items(menu, menu_items, is_active, view, window, cx); diff --git a/crates/db_view/src/er_diagram/mod.rs b/crates/db_view/src/er_diagram/mod.rs index bd0d597b90..8a02654c14 100644 --- a/crates/db_view/src/er_diagram/mod.rs +++ b/crates/db_view/src/er_diagram/mod.rs @@ -1,18 +1,17 @@ mod loader; mod pan_mode_plugin; mod scroll_pan_plugin; +mod scrollbar_plugin; use db::GlobalDbState; use ferrum_flow::{ - BackgroundPlugin, EdgePlugin, FitAllGraphPlugin, FlowCanvas, FlowTheme, Graph, MinimapPlugin, + BackgroundPlugin, EdgePlugin, FitAllGraphPlugin, FlowCanvas, Graph, MinimapPlugin, NodeInteractionPlugin, NodePlugin, ViewportPlugin, ZoomControlsPlugin, }; use crate::er_diagram::pan_mode_plugin::ErDiagramPanModePlugin; use crate::er_diagram::scroll_pan_plugin::ErDiagramScrollPanPlugin; -use er_flow::{ - ErCardTheme, er_flow_theme_from_ui, er_node_renderers_from_theme, graph_from_diagram, -}; +use er_flow::{er_flow_theme_from_ui, er_node_renderers_from_theme, graph_from_diagram}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, Styled as _, @@ -42,16 +41,9 @@ pub(crate) struct ErDiagramTab { canvas: Option>, loading: bool, error: Option, - theme_snapshot: Option, focus_handle: FocusHandle, } -#[derive(Clone, PartialEq)] -struct ErDiagramThemeSnapshot { - flow_theme: FlowTheme, - card_theme: ErCardTheme, -} - impl ErDiagramTab { pub(crate) fn new( config: ErDiagramConfig, @@ -63,7 +55,6 @@ impl ErDiagramTab { canvas: None, loading: true, error: None, - theme_snapshot: None, focus_handle: cx.focus_handle(), }; tab.reload(window, cx); @@ -108,27 +99,9 @@ impl ErDiagramTab { self.error = Some(err.to_string()); } } - self.theme_snapshot = Some(current_theme_snapshot(cx)); cx.notify(); } - fn sync_canvas_theme(&mut self, cx: &mut Context) { - let Some(canvas) = self.canvas.as_ref() else { - return; - }; - let next_snapshot = current_theme_snapshot(cx); - if self.theme_snapshot.as_ref() == Some(&next_snapshot) { - return; - } - let theme = cx.theme(); - let renderers = er_node_renderers_from_theme(theme); - canvas.update(cx, |canvas, cx| { - canvas.set_theme(next_snapshot.flow_theme.clone(), cx); - canvas.replace_node_renderers(renderers, cx); - }); - self.theme_snapshot = Some(next_snapshot); - } - fn render_loading(&self, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() @@ -248,8 +221,6 @@ fn build_canvas( impl Render for ErDiagramTab { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.sync_canvas_theme(cx); - div() .track_focus(&self.focus_handle) .size_full() @@ -266,14 +237,6 @@ impl Render for ErDiagramTab { } } -fn current_theme_snapshot(cx: &mut App) -> ErDiagramThemeSnapshot { - let theme = cx.theme(); - ErDiagramThemeSnapshot { - flow_theme: er_flow_theme_from_ui(theme), - card_theme: ErCardTheme::from_ui_theme(theme), - } -} - impl Focusable for ErDiagramTab { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() diff --git a/crates/db_view/src/er_diagram/pan_mode_plugin.rs b/crates/db_view/src/er_diagram/pan_mode_plugin.rs index 649ee7105c..1d8508ddcd 100644 --- a/crates/db_view/src/er_diagram/pan_mode_plugin.rs +++ b/crates/db_view/src/er_diagram/pan_mode_plugin.rs @@ -90,9 +90,10 @@ impl Plugin for ErDiagramPanModePlugin { if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event && ev.button == MouseButton::Left { + let pointer_position = ctx.window_pointer_to_canvas_local(ev.position); if self .last_bounds - .is_some_and(|bounds| bounds.contains(&ev.position)) + .is_some_and(|bounds| bounds.contains(&pointer_position)) { let active = ctx .shared_state diff --git a/crates/db_view/src/er_diagram/scroll_pan_plugin.rs b/crates/db_view/src/er_diagram/scroll_pan_plugin.rs index a49457a8fd..f1a61244e3 100644 --- a/crates/db_view/src/er_diagram/scroll_pan_plugin.rs +++ b/crates/db_view/src/er_diagram/scroll_pan_plugin.rs @@ -1,11 +1,23 @@ -use ferrum_flow::{EventResult, FlowEvent, InputEvent, Plugin, PluginContext}; -use gpui::{Pixels, Point, px}; +use std::time::Duration; -pub struct ErDiagramScrollPanPlugin; +use ferrum_flow::{ + EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer, +}; +use gpui::{MouseButton, Pixels, Point, px}; + +use crate::er_diagram::scrollbar_plugin::{ScrollbarDragInteraction, ScrollbarState}; + +pub struct ErDiagramScrollPanPlugin { + scrollbars: ScrollbarState, + refresh_scheduled: bool, +} impl ErDiagramScrollPanPlugin { pub fn new() -> Self { - Self + Self { + scrollbars: ScrollbarState::default(), + refresh_scheduled: false, + } } } @@ -15,23 +27,51 @@ impl Plugin for ErDiagramScrollPanPlugin { } fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let FlowEvent::Input(InputEvent::Wheel(ev)) = event { - let delta = ev.delta.pixel_delta(px(1.0)); - let pan = wheel_delta_to_pan(delta); - let dx = pan.x; - let dy = pan.y; - if dx != px(0.0) || dy != px(0.0) { - ctx.translate_offset(dx, dy); - ctx.notify(); - return EventResult::Stop; + match event { + FlowEvent::DrawableBoundsReady => { + if !self.refresh_scheduled { + self.refresh_scheduled = true; + ctx.cache_all_node_port_offset(); + ctx.notify(); + ctx.schedule_after(Duration::from_millis(16)); + } + EventResult::Continue } + FlowEvent::Input(InputEvent::Wheel(ev)) => { + let delta = ev.delta.pixel_delta(px(1.0)); + let pan = wheel_delta_to_pan(delta); + let dx = pan.x; + let dy = pan.y; + if dx != px(0.0) || dy != px(0.0) { + ctx.translate_offset(dx, dy); + ctx.notify(); + return EventResult::Stop; + } + EventResult::Continue + } + FlowEvent::Input(InputEvent::MouseDown(ev)) if ev.button == MouseButton::Left => { + let pointer_position = ctx.window_pointer_to_canvas_local(ev.position); + if let Some(axis) = self.scrollbars.axis_at(pointer_position) { + ctx.start_interaction(ScrollbarDragInteraction::new(axis, ev.position, ctx)); + return EventResult::Stop; + } + EventResult::Continue + } + _ => EventResult::Continue, } - EventResult::Continue + } + + fn render(&mut self, ctx: &mut RenderContext) -> Option { + self.scrollbars.render(ctx) } fn priority(&self) -> i32 { 130 } + + fn render_layer(&self) -> RenderLayer { + RenderLayer::Overlay + } } fn wheel_delta_to_pan(delta: Point) -> Point { @@ -41,6 +81,7 @@ fn wheel_delta_to_pan(delta: Point) -> Point { #[cfg(test)] mod tests { use super::wheel_delta_to_pan; + use crate::er_diagram::scrollbar_plugin::thumb_length; use gpui::{Point, px}; #[test] @@ -50,4 +91,9 @@ mod tests { Point::new(px(0.0), px(24.0)) ); } + + #[test] + fn thumb_length_has_minimum_size() { + assert_eq!(thumb_length(px(100.0), px(10.0), px(1000.0)), px(32.0)); + } } diff --git a/crates/db_view/src/er_diagram/scrollbar_plugin.rs b/crates/db_view/src/er_diagram/scrollbar_plugin.rs new file mode 100644 index 0000000000..0f389f59c6 --- /dev/null +++ b/crates/db_view/src/er_diagram/scrollbar_plugin.rs @@ -0,0 +1,311 @@ +use ferrum_flow::{Interaction, InteractionResult, PluginContext, RenderContext}; +use gpui::{ + Bounds, IntoElement, ParentElement as _, Pixels, Point, Size, Styled as _, div, hsla, px, +}; + +const SCROLLBAR_MARGIN: f32 = 8.0; +const SCROLLBAR_THICKNESS: f32 = 8.0; +const SCROLLBAR_MIN_THUMB: f32 = 32.0; +const CONTENT_PADDING: f32 = 80.0; + +#[derive(Clone, Copy)] +pub(super) enum ScrollbarAxis { + Horizontal, + Vertical, +} + +#[derive(Default)] +pub(super) struct ScrollbarState { + horizontal_thumb: Option>, + vertical_thumb: Option>, +} + +impl ScrollbarState { + pub(super) fn axis_at(&self, position: Point) -> Option { + if self + .horizontal_thumb + .is_some_and(|bounds| bounds.contains(&position)) + { + return Some(ScrollbarAxis::Horizontal); + } + if self + .vertical_thumb + .is_some_and(|bounds| bounds.contains(&position)) + { + return Some(ScrollbarAxis::Vertical); + } + None + } + + pub(super) fn render(&mut self, ctx: &mut RenderContext) -> Option { + let Some(metrics) = ScrollbarMetrics::from_render_context(ctx) else { + self.horizontal_thumb = None; + self.vertical_thumb = None; + return None; + }; + self.horizontal_thumb = metrics.horizontal_thumb; + self.vertical_thumb = metrics.vertical_thumb; + + let track_color = hsla(0.0, 0.0, 0.0, 0.18); + let thumb_color = hsla(0.0, 0.0, 0.45, 0.55); + Some( + div() + .absolute() + .size_full() + .child(render_track(metrics.horizontal_track, track_color)) + .child(render_track(metrics.vertical_track, track_color)) + .child(render_thumb(metrics.horizontal_thumb, thumb_color)) + .child(render_thumb(metrics.vertical_thumb, thumb_color)) + .into_any_element(), + ) + } +} + +fn render_track(track: Option>, color: gpui::Hsla) -> impl IntoElement { + div().children(track.map(|track| render_bar(track, color))) +} + +fn render_thumb(thumb: Option>, color: gpui::Hsla) -> impl IntoElement { + div().children(thumb.map(|thumb| render_bar(thumb, color))) +} + +fn render_bar(bounds: Bounds, color: gpui::Hsla) -> impl IntoElement { + div() + .absolute() + .left(bounds.origin.x) + .top(bounds.origin.y) + .w(bounds.size.width) + .h(bounds.size.height) + .rounded(px(SCROLLBAR_THICKNESS / 2.0)) + .bg(color) +} + +pub(super) struct ScrollbarDragInteraction { + axis: ScrollbarAxis, + start_mouse: Point, + start_offset: Point, + world_bounds: Option, + window_bounds: Option>, + zoom: f32, +} + +impl ScrollbarDragInteraction { + pub(super) fn new( + axis: ScrollbarAxis, + start_mouse: Point, + ctx: &PluginContext, + ) -> Self { + Self { + axis, + start_mouse, + start_offset: ctx.offset(), + world_bounds: graph_world_bounds(ctx), + window_bounds: ctx.window_bounds(), + zoom: ctx.zoom(), + } + } +} + +impl Interaction for ScrollbarDragInteraction { + fn on_mouse_move( + &mut self, + ev: &gpui::MouseMoveEvent, + ctx: &mut PluginContext, + ) -> InteractionResult { + let Some(bounds) = self.world_bounds else { + return InteractionResult::End; + }; + let Some(window_bounds) = self.window_bounds else { + return InteractionResult::End; + }; + let next_offset = match self.axis { + ScrollbarAxis::Horizontal => { + let delta = ev.position.x - self.start_mouse.x; + let content_width = bounds.width * self.zoom; + let track_width = scrollbar_horizontal_track(window_bounds).size.width; + let movable = (track_width + - thumb_length(track_width, window_bounds.size.width, content_width)) + .max(px(1.0)); + let scrollable = (content_width - window_bounds.size.width).max(px(1.0)); + Point::new( + self.start_offset.x - delta * pixel_ratio(scrollable, movable), + self.start_offset.y, + ) + } + ScrollbarAxis::Vertical => { + let delta = ev.position.y - self.start_mouse.y; + let content_height = bounds.height * self.zoom; + let track_height = scrollbar_vertical_track(window_bounds).size.height; + let movable = (track_height + - thumb_length(track_height, window_bounds.size.height, content_height)) + .max(px(1.0)); + let scrollable = (content_height - window_bounds.size.height).max(px(1.0)); + Point::new( + self.start_offset.x, + self.start_offset.y - delta * pixel_ratio(scrollable, movable), + ) + } + }; + ctx.set_offset(next_offset); + ctx.notify(); + InteractionResult::Continue + } + + fn on_mouse_up( + &mut self, + _event: &gpui::MouseUpEvent, + ctx: &mut PluginContext, + ) -> InteractionResult { + ctx.cancel_interaction(); + InteractionResult::End + } +} + +#[derive(Clone, Copy)] +struct WorldBounds { + min_x: f32, + min_y: f32, + width: Pixels, + height: Pixels, +} + +struct ScrollbarMetrics { + horizontal_track: Option>, + horizontal_thumb: Option>, + vertical_track: Option>, + vertical_thumb: Option>, +} + +impl ScrollbarMetrics { + fn from_render_context(ctx: &RenderContext) -> Option { + let bounds = graph_world_bounds_from_render(ctx)?; + let window_bounds = ctx.window_bounds()?; + let zoom = ctx.zoom(); + let content_width = bounds.width * zoom; + let content_height = bounds.height * zoom; + let horizontal_track = (content_width > window_bounds.size.width) + .then(|| scrollbar_horizontal_track(window_bounds)); + let vertical_track = (content_height > window_bounds.size.height) + .then(|| scrollbar_vertical_track(window_bounds)); + let horizontal_thumb = horizontal_track.map(|track| { + horizontal_thumb_bounds( + track, + window_bounds.size.width, + content_width, + bounds, + zoom, + ctx.offset().x, + ) + }); + let vertical_thumb = vertical_track.map(|track| { + vertical_thumb_bounds( + track, + window_bounds.size.height, + content_height, + bounds, + zoom, + ctx.offset().y, + ) + }); + Some(Self { + horizontal_track, + horizontal_thumb, + vertical_track, + vertical_thumb, + }) + } +} + +fn graph_world_bounds(ctx: &PluginContext) -> Option { + ctx.graph.nodes_world_aabb().map(world_bounds_from_aabb) +} + +fn graph_world_bounds_from_render(ctx: &RenderContext) -> Option { + ctx.graph.nodes_world_aabb().map(world_bounds_from_aabb) +} + +fn world_bounds_from_aabb((min_x, min_y, width, height): (f32, f32, f32, f32)) -> WorldBounds { + WorldBounds { + min_x: min_x - CONTENT_PADDING, + min_y: min_y - CONTENT_PADDING, + width: px(width + 2.0 * CONTENT_PADDING), + height: px(height + 2.0 * CONTENT_PADDING), + } +} + +fn scrollbar_horizontal_track(window_bounds: Bounds) -> Bounds { + Bounds::new( + Point::new( + px(SCROLLBAR_MARGIN), + window_bounds.size.height - px(SCROLLBAR_MARGIN + SCROLLBAR_THICKNESS), + ), + Size::new( + window_bounds.size.width - px(2.0 * SCROLLBAR_MARGIN + SCROLLBAR_THICKNESS), + px(SCROLLBAR_THICKNESS), + ), + ) +} + +fn scrollbar_vertical_track(window_bounds: Bounds) -> Bounds { + Bounds::new( + Point::new( + window_bounds.size.width - px(SCROLLBAR_MARGIN + SCROLLBAR_THICKNESS), + px(SCROLLBAR_MARGIN), + ), + Size::new( + px(SCROLLBAR_THICKNESS), + window_bounds.size.height - px(2.0 * SCROLLBAR_MARGIN + SCROLLBAR_THICKNESS), + ), + ) +} + +fn horizontal_thumb_bounds( + track: Bounds, + viewport_width: Pixels, + content_width: Pixels, + bounds: WorldBounds, + zoom: f32, + offset_x: Pixels, +) -> Bounds { + let length = thumb_length(track.size.width, viewport_width, content_width); + let movable = (track.size.width - length).max(px(0.0)); + let scrollable = (content_width - viewport_width).max(px(1.0)); + let content_start = px(bounds.min_x) * zoom + offset_x; + let ratio = (-content_start / scrollable).clamp(0.0, 1.0); + Bounds::new( + Point::new(track.origin.x + movable * ratio, track.origin.y), + Size::new(length, track.size.height), + ) +} + +fn vertical_thumb_bounds( + track: Bounds, + viewport_height: Pixels, + content_height: Pixels, + bounds: WorldBounds, + zoom: f32, + offset_y: Pixels, +) -> Bounds { + let length = thumb_length(track.size.height, viewport_height, content_height); + let movable = (track.size.height - length).max(px(0.0)); + let scrollable = (content_height - viewport_height).max(px(1.0)); + let content_start = px(bounds.min_y) * zoom + offset_y; + let ratio = (-content_start / scrollable).clamp(0.0, 1.0); + Bounds::new( + Point::new(track.origin.x, track.origin.y + movable * ratio), + Size::new(track.size.width, length), + ) +} + +pub(super) fn thumb_length( + track_length: Pixels, + viewport_length: Pixels, + content_length: Pixels, +) -> Pixels { + (track_length * pixel_ratio(viewport_length, content_length)) + .clamp(px(SCROLLBAR_MIN_THUMB), track_length) +} + +fn pixel_ratio(numerator: Pixels, denominator: Pixels) -> f32 { + f32::from(numerator) / f32::from(denominator) +} diff --git a/crates/db_view/src/import_export/sql_dump_view.rs b/crates/db_view/src/import_export/sql_dump_view.rs index 3e78302e58..1bba7e851c 100644 --- a/crates/db_view/src/import_export/sql_dump_view.rs +++ b/crates/db_view/src/import_export/sql_dump_view.rs @@ -3,7 +3,7 @@ use gpui::{ IntoElement, ParentElement, Render, Styled, Window, div, prelude::FluentBuilder, px, }; use gpui_component::{ - ActiveTheme, VirtualListScrollHandle, + ActiveTheme, TitleBar, VirtualListScrollHandle, button::{Button, ButtonVariants as _}, h_flex, v_flex, v_virtual_list, }; @@ -506,12 +506,11 @@ impl Render for SqlDumpView { let elapsed = self.elapsed_time.read(cx).clone(); let logs = self.logs.read(cx).clone(); - v_flex() + let content = v_flex() .w_full() .h(px(450.0)) .gap_3() .p_4() - .pt_8() .child( v_flex() .gap_1() @@ -708,6 +707,12 @@ impl Render for SqlDumpView { }), ) }), - ) + ); + + v_flex() + .w_full() + .h(px(510.0)) + .child(TitleBar::new()) + .child(content) } } diff --git a/crates/db_view/src/import_export/sql_run_view.rs b/crates/db_view/src/import_export/sql_run_view.rs index f956d944b0..e478239b99 100644 --- a/crates/db_view/src/import_export/sql_run_view.rs +++ b/crates/db_view/src/import_export/sql_run_view.rs @@ -9,7 +9,7 @@ use gpui::{ prelude::FluentBuilder, px, }; use gpui_component::{ - ActiveTheme, Disableable, Sizable, VirtualListScrollHandle, + ActiveTheme, Disableable, Sizable, TitleBar, VirtualListScrollHandle, button::{Button, ButtonVariants as _}, h_flex, input::{Input, InputState}, @@ -453,12 +453,11 @@ impl Render for SqlRunView { let elapsed = self.elapsed_time.read(cx).clone(); let logs = self.logs.read(cx).clone(); - v_flex() + let content = v_flex() .w_full() .h(px(500.0)) .gap_3() .p_4() - .pt_8() .child( h_flex() .gap_2() @@ -687,6 +686,12 @@ impl Render for SqlRunView { }), ) }), - ) + ); + + v_flex() + .w_full() + .h(px(520.0)) + .child(TitleBar::new()) + .child(content) } } diff --git a/crates/db_view/src/import_export/table_export_view.rs b/crates/db_view/src/import_export/table_export_view.rs index 64bc01c8e5..1042ec262b 100644 --- a/crates/db_view/src/import_export/table_export_view.rs +++ b/crates/db_view/src/import_export/table_export_view.rs @@ -9,7 +9,7 @@ use gpui::{ px, }; use gpui_component::{ - ActiveTheme, Disableable, IconName, IndexPath, Sizable, VirtualListScrollHandle, + ActiveTheme, Disableable, IconName, IndexPath, Sizable, TitleBar, VirtualListScrollHandle, button::{Button, ButtonVariants as _}, checkbox::Checkbox, h_flex, @@ -876,12 +876,11 @@ impl Render for DataExportView { let logs = self.logs.read(cx).clone(); let current_step = self.current_step; - v_flex() + let content = v_flex() .w_full() .h(px(540.0)) .gap_2() .p_4() - .pt_8() .child( div() .text_sm() @@ -1351,6 +1350,12 @@ impl Render for DataExportView { }) ) }), - ) + ); + + v_flex() + .w_full() + .h(px(600.0)) + .child(TitleBar::new()) + .child(content) } } diff --git a/crates/db_view/src/import_export/table_import_view.rs b/crates/db_view/src/import_export/table_import_view.rs index 288aac4c69..912c54edad 100644 --- a/crates/db_view/src/import_export/table_import_view.rs +++ b/crates/db_view/src/import_export/table_import_view.rs @@ -7,7 +7,7 @@ use gpui::{ Window, div, prelude::FluentBuilder, px, }; use gpui_component::{ - ActiveTheme, Disableable, IconName, IndexPath, VirtualListScrollHandle, + ActiveTheme, Disableable, IconName, IndexPath, TitleBar, VirtualListScrollHandle, button::{Button, ButtonVariants as _}, h_flex, input::{Input, InputState}, @@ -792,12 +792,11 @@ impl Render for TableImportView { let current_step = self.current_step; let validation_error = self.validation_error.read(cx).clone(); - v_flex() + let content = v_flex() .w_full() .h(px(540.0)) .gap_3() .p_4() - .pt_8() .child( div() .text_sm() @@ -1371,6 +1370,12 @@ impl Render for TableImportView { }, )) }), - ) + ); + + v_flex() + .w_full() + .h(px(600.0)) + .child(TitleBar::new()) + .child(content) } } diff --git a/crates/db_view/src/sql_editor_view.rs b/crates/db_view/src/sql_editor_view.rs index 23d93ff1d6..9b36d926fa 100644 --- a/crates/db_view/src/sql_editor_view.rs +++ b/crates/db_view/src/sql_editor_view.rs @@ -81,8 +81,9 @@ impl SqlEditorTab { cx.new(|cx| SelectState::new(SearchableVec::new(vec![]), None, window, cx)); let global_state = cx.global::().clone(); - let supports_schema = global_state.supports_schema(&database_type); - let uses_schema_as_database = global_state.uses_schema_as_database(&database_type); + let capabilities = global_state.capabilities(&database_type); + let supports_schema = capabilities.supports_schema; + let uses_schema_as_database = capabilities.uses_schema_as_database; let connection_id_str = connection_id.into(); let should_load_file = file_path.is_some(); diff --git a/crates/db_view/src/table_data/filter_editor.rs b/crates/db_view/src/table_data/filter_editor.rs index 071bbf385d..7de1adfeb5 100644 --- a/crates/db_view/src/table_data/filter_editor.rs +++ b/crates/db_view/src/table_data/filter_editor.rs @@ -1739,11 +1739,16 @@ impl VisualFilterBuilder { let value_end_input = self.value_end_inputs.get(&row.id); let logic_is_and = matches!(row.logic_operator, LogicOperator::And); + const LOGIC_TOGGLE_WIDTH: gpui::Pixels = px(48.); + // 逻辑操作符选择器(根级别的第一条条件不显示) let logic_toggle = if cr.idx > 0 { gpui::div() + .w(LOGIC_TOGGLE_WIDTH) + .flex() + .justify_center() + .items_center() .text_xs() - .px_2() .py_px() .rounded_full() .bg(if logic_is_and { @@ -1761,7 +1766,7 @@ impl VisualFilterBuilder { ) .child(if logic_is_and { "AND" } else { "OR" }) } else { - gpui::div().w(px(48.)) + gpui::div().w(LOGIC_TOGGLE_WIDTH) }; // 值输入区域 diff --git a/crates/db_view/src/table_designer_tab.rs b/crates/db_view/src/table_designer_tab.rs index ef75575776..be16117716 100644 --- a/crates/db_view/src/table_designer_tab.rs +++ b/crates/db_view/src/table_designer_tab.rs @@ -319,11 +319,11 @@ impl TableDesigner { Vec, ColumnEditorCapabilities, ) = { - let engines = get_engines_for(config.database_type) + let engines = get_engines_for(config.database_type, cx) .into_iter() .map(|name| EngineSelectItem { name }) .collect(); - let capabilities = get_column_editor_capabilities_for(config.database_type); + let capabilities = get_column_editor_capabilities_for(config.database_type, cx); (engines, capabilities) }; @@ -1175,7 +1175,7 @@ impl TableDesigner { } fn render_options(&self, cx: &Context) -> AnyElement { - let capabilities = get_table_designer_capabilities_for(self.config.database_type); + let capabilities = get_table_designer_capabilities_for(self.config.database_type, cx); v_flex() .size_full() diff --git a/crates/duckdb_driver/src/main.rs b/crates/duckdb_driver/src/main.rs index e3bb0b4964..3d2836528d 100644 --- a/crates/duckdb_driver/src/main.rs +++ b/crates/duckdb_driver/src/main.rs @@ -6,9 +6,10 @@ async fn main() -> Result<()> { .with_writer(std::io::stderr) .init(); - let socket_name = std::env::args() - .nth(1) - .or_else(|| std::env::var("ONETCLI_DUCKDB_DRIVER_SOCKET").ok()) + let socket_name = std::env::var("ONETCLI_IPC_SOCKET") + .or_else(|_| std::env::var("ONETCLI_DUCKDB_DRIVER_SOCKET")) + .ok() + .or_else(|| std::env::args().nth(1)) .unwrap_or_else(|| "onetcli-duckdb-driver.sock".to_string()); duckdb_driver::server::run(&socket_name).await diff --git a/crates/duckdb_driver/src/metadata.rs b/crates/duckdb_driver/src/metadata.rs index e9abcfc841..75c00c7da4 100644 --- a/crates/duckdb_driver/src/metadata.rs +++ b/crates/duckdb_driver/src/metadata.rs @@ -54,17 +54,24 @@ struct ViewInfo { comment: Option, } -pub fn handle(session: &DuckDbSession, method: &str, params: &Value) -> Result { +pub fn handle(session: &DuckDbSession, method: &str, params: &Value) -> Result> { let connection = session.connection()?; match method { - "metadata.list_databases" => Ok(json!(vec!["main"])), - "metadata.list_databases_detailed" => to_value(list_databases_detailed()), - "metadata.list_schemas" => to_value(list_schemas(connection)?), - "metadata.list_tables" => to_value(list_tables(connection, params)?), - "metadata.list_columns" => to_value(list_columns(connection, params)?), - "metadata.list_indexes" => to_value(list_indexes(connection, params)?), - "metadata.list_views" => to_value(list_views(connection, params)?), - _ => anyhow::bail!("unsupported metadata method: {method}"), + "metadata.list_databases" => Ok(Some(json!(vec!["main"]))), + "metadata.list_databases_detailed" => to_value(list_databases_detailed()).map(Some), + "metadata.list_schemas" => to_value(list_schemas(connection)?).map(Some), + "metadata.list_tables" => to_value(list_tables(connection, params)?).map(Some), + "metadata.list_columns" => to_value(list_columns(connection, params)?).map(Some), + "metadata.list_indexes" => to_value(list_indexes(connection, params)?).map(Some), + "metadata.list_views" => to_value(list_views(connection, params)?).map(Some), + "metadata.list_functions" + | "metadata.list_procedures" + | "metadata.list_triggers" + | "metadata.list_sequences" + | "metadata.list_foreign_keys" + | "metadata.list_table_triggers" + | "metadata.list_table_checks" => Ok(Some(json!([]))), + _ => Ok(None), } } diff --git a/crates/duckdb_driver/src/server.rs b/crates/duckdb_driver/src/server.rs index 1ea2ffb06b..b5aba48ae6 100644 --- a/crates/duckdb_driver/src/server.rs +++ b/crates/duckdb_driver/src/server.rs @@ -45,7 +45,15 @@ async fn handle_connection(mut stream: Stream) -> Result<()> { let response = match handle_request(&mut session, &request) { Ok(result) => IpcResponse::result(request_id, result), - Err(error) => IpcResponse::error(request_id, IpcErrorCode::Internal, error.to_string()), + Err(error) => { + let message = error.to_string(); + let code = if message.starts_with("unsupported method:") { + IpcErrorCode::UnsupportedMethod + } else { + IpcErrorCode::Internal + }; + IpcResponse::error(request_id, code, message) + } }; send_msg_async(&mut stream, &response).await?; @@ -80,7 +88,8 @@ fn handle_request(session: &mut DuckDbSession, request: &IpcRequest) -> Result { - crate::metadata::handle(session, method, &request.params) + crate::metadata::handle(session, method, &request.params)? + .ok_or_else(|| anyhow::anyhow!("unsupported method: {method}")) } method => anyhow::bail!("unsupported method: {method}"), } diff --git a/crates/ferrum-flow/Cargo.toml b/crates/ferrum-flow/Cargo.toml deleted file mode 100644 index e17528df32..0000000000 --- a/crates/ferrum-flow/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ferrum-flow" -version = "0.2.1" -edition = "2024" -license = "Apache-2.0" -authors = ["tu6ge"] -repository = "https://github.com/tu6ge/ferrum-flow" -description = "A high-performance node-based editor framework built with Rust and GPUI." - -[features] -default = [] -## Public `command_interop` test helpers; run `cargo test -p ferrum-flow --features testing`. -testing = [] - -[dependencies] -anyhow = { workspace = true } -gpui = { workspace = true } -# used by background plugin -image = { workspace = true } -# used by background plugin -smallvec = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -futures = { workspace = true, features = ["std"] } -uuid = { workspace = true } diff --git a/crates/ferrum-flow/README.md b/crates/ferrum-flow/README.md deleted file mode 100644 index 3f9523a29b..0000000000 --- a/crates/ferrum-flow/README.md +++ /dev/null @@ -1,240 +0,0 @@ -# FerrumFlow - -A high-performance, extensible node-based editor built with Rust and gpui. -Designed for building visual programming tools, workflow editors, and graph-based UIs. - -**This project is in early stage (alpha), API may change** - -## Features - -- Plugin-based architecture -- Interaction system (drag, pan, select, etc.) -- Undo / Redo (Command pattern) -- Viewport control (zoom & pan) -- Box selection & multi-select -- Node / Port / Edge model -- Custom node rendering system -- Built with performance in mind -- Multi-user collaboration support (by [plugin](https://github.com/tu6ge/ferrum-flow/tree/master/crates/sync_plugin)) - -[![Watch the video](https://img.youtube.com/vi/mimeKsIldog/0.jpg)](https://www.youtube.com/watch?v=mimeKsIldog) - -[GitHub](https://github.com/tu6ge/ferrum-flow) - -## Usage - -```bash -cargo add ferrum-flow -``` - -This is a hello world example: - -```rust -use ferrum_flow::{FlowCanvas, Graph}; -use gpui::{AppContext as _, Application, WindowOptions}; -use serde_json::json; - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("default") - .position(100.0, 100.0) - .data(json!({ "label": "Hello World" })) - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .default_plugins() // Includes built-in rendering for nodes, edges, selection, and more. Replace with custom plugins as needed. - .build() - }) - }) - .unwrap(); - }); -} -``` - -For more examples, see the [examples directory](./examples/). - -## Architecture Overview - -The system is designed with clear separation of concerns: - -### Core Concepts - -- Graph - Stores persistent data (nodes, edges, ports) - -- Viewport - Handles zooming and panning - -- Plugin System - Extends behavior (rendering, input handling, etc.) -- Interaction System - Manages ongoing user interactions (dragging, selecting, etc.) - -- Command System - Enables undo/redo support - -### Plugin System - -Plugins are the primary extension mechanism: - -```rust -pub trait Plugin { - fn name(&self) -> &'static str; - - fn setup(&mut self, ctx: &mut InitPluginContext); - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult; - - fn render(&mut self, ctx: &mut RenderContext) -> Option; - - fn priority(&self) -> i32 { - 0 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } -} -``` - -**Responsibilities** - -A plugin can: - -- Handle input events -- Start interactions -- Render UI layers -- Modify graph state - -### Interaction System - -Interactions represent ongoing user actions, such as: - -- Node dragging -- Box selection -- Viewport panning - -```rust -pub trait Interaction { - fn on_mouse_move(&mut self, event: &MouseMoveEvent, ctx: &mut PluginContext) -> InteractionResult; - - fn on_mouse_up(&mut self, event: &MouseUpEvent, ctx: &mut PluginContext) -> InteractionResult; - - fn render(&self, ctx: &mut RenderContext) -> Option; -} -``` - -Interaction Lifecycle - -``` -Start → Update → End / Replace -``` - -```rust -pub enum InteractionResult { - Continue, - End, - Replace(Box), -} -``` - -### Command System (Undo / Redo) - -Implements the Command Pattern: - -```rust -pub trait Command { - fn execute(&mut self, ctx: &mut CommandContext); - fn undo(&mut self, ctx: &mut CommandContext); -} -``` - -Built-in Features - -- Undo / Redo stacks -- Composite commands -- Easy integration via PluginContext - -```rust -ctx.execute_command(MyCommand { ... }); -``` - -### Node Rendering - -Rendering is fully customizable via a registry: - -```rust -pub trait NodeRenderer { - fn render(&self, node: &Node, ctx: &mut RenderContext) -> AnyElement; - - // custom render port UI - fn port_render(&self, node: &Node, port: &Port, ctx: &mut RenderContext) -> Option { - // ... default implement - } - - // computing the position of port relative to node - fn port_offset(&self, node: &Node, port: &Port, graph: &Graph) -> Point { - // ... default implement - } -} -``` - -Render example: - -```rust -// Absolute-positioned node card shell: screen origin, zoom-scaled size. -ctx.node_card_shell(node, false, NodeCardVariant::Custom) - .rounded(px(6.0)) - .border(px(1.5)) -``` - -### Graph Model - -```rust -pub struct Node { - id: NodeId, - node_type: String, - x: Pixels, - y: Pixels, - size: Size, - inputs: Vec, - outputs: Vec, - data: serde_json::Value, -} -``` - -🏗️ Creating Nodes (Builder API) - -```rust -graph.create_node("math.add") - .position(100.0, 100.0) - .input() - .output() - .build(); -``` - -### Performance - -Designed to scale to large graphs: - -- Viewport-based rendering (virtualization) -- Layered rendering system -- Interaction-aware rendering (degraded mode during drag) -- Ready for spatial indexing - -### Design Principles - -- Separation of data and interaction -- Plugins over hardcoded behavior -- Explicit state transitions -- Performance-first rendering -- Composable architecture - -## License - -Apache2.0 diff --git a/crates/ferrum-flow/examples/basic.rs b/crates/ferrum-flow/examples/basic.rs deleted file mode 100644 index fd188ea481..0000000000 --- a/crates/ferrum-flow/examples/basic.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ferrum_flow::{FlowCanvas, Graph}; -use gpui::{AppContext as _, Application, WindowOptions}; -use serde_json::json; - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("default") - .position(100.0, 100.0) - .data(json!({ "label": "Hello World" })) - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .default_plugins() - .build() - }) - }) - .unwrap(); - }); -} diff --git a/crates/ferrum-flow/examples/basic2.rs b/crates/ferrum-flow/examples/basic2.rs deleted file mode 100644 index 95e5095421..0000000000 --- a/crates/ferrum-flow/examples/basic2.rs +++ /dev/null @@ -1,58 +0,0 @@ -use ferrum_flow::*; -use gpui::{AppContext as _, Application, Size, WindowOptions, px}; -use serde_json::json; - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("") - .position(100.0, 100.0) - .output() - .output() - .output_with(PortPosition::Bottom, Size::new(px(20.0), px(20.0))) - .output_at(PortPosition::Bottom) - .data(json!({ "label": "Node 1" })) - .build(); - - graph - .create_node("") - .position(300.0, 400.0) - .input() - .input_at(PortPosition::Top) - .input_at(PortPosition::Top) - .output() - .output_at(PortPosition::Bottom) - .output_at(PortPosition::Bottom) - .data(json!({ "label": "Node 2" })) - .build(); - - graph - .create_node("") - .position(500.0, 500.0) - .input() - .output() - .data(json!({ "label": "Node 3" })) - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .default_plugins() - .plugin(MinimapPlugin::new()) - .plugin(ZoomControlsPlugin::new()) - .plugin(ClipboardPlugin::new()) - .plugin(ContextMenuPlugin::new()) - .plugin(SelectAllViewportPlugin::new()) - .plugin(AlignPlugin::new()) - .plugin(FocusSelectionPlugin::new()) - .plugin(FitAllGraphPlugin::new()) - .plugin(SnapGuidesPlugin::new()) - .plugin(ToastPlugin::new()) - .build() - }) - }) - .unwrap(); - }); -} diff --git a/crates/ferrum-flow/examples/bench.rs b/crates/ferrum-flow/examples/bench.rs deleted file mode 100644 index 8bd367f290..0000000000 --- a/crates/ferrum-flow/examples/bench.rs +++ /dev/null @@ -1,71 +0,0 @@ -use ferrum_flow::*; -use gpui::{AppContext as _, Application, WindowOptions}; -use serde_json::json; - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - for j in 0..100 { - for i in 0..100 { - graph - .create_node("") - .position(200.0 * i as f32, 200.0 * j as f32) - .input() - .output() - .data(json!({ "label": format!("Node {}", i * 100 + j) })) - .build(); - } - } - - let node_ids = graph.nodes().keys().copied().collect::>(); - - generate_chain_edges(&mut graph, node_ids); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .plugin(BackgroundPlugin::new()) - .plugin(SelectionPlugin::new()) - .plugin(NodeInteractionPlugin::new()) - .plugin(ViewportPlugin::new()) - .plugin(NodePlugin::new()) - .plugin(PortInteractionPlugin::new()) - .plugin(EdgePlugin::new()) - .plugin(DeletePlugin::new()) - .plugin(HistoryPlugin::new()) - .plugin(MinimapPlugin::new()) - .plugin(ClipboardPlugin::new()) - .plugin(ContextMenuPlugin::new()) - .plugin(SelectAllViewportPlugin::new()) - .plugin(AlignPlugin::new()) - .plugin(FocusSelectionPlugin::new()) - .plugin(ZoomControlsPlugin::new()) - .plugin(SnapGuidesPlugin::new()) - .plugin(ToastPlugin::new()) - //.plugin(FitAllGraphPlugin::new()) - .build() - }) - }) - .unwrap(); - }); -} - -pub fn generate_chain_edges(graph: &mut Graph, node_ids: Vec) { - for window in node_ids.windows(2) { - let from = window[0]; - let to = window[1]; - - let from_node = graph.get_node(&from).unwrap(); - let to_node = graph.get_node(&to).unwrap(); - - let source_port = from_node.outputs()[0]; - let target_port = to_node.inputs()[0]; - - graph - .create_edge() - .source(source_port) - .target(target_port) - .build(); - } -} diff --git a/crates/ferrum-flow/examples/extension.rs b/crates/ferrum-flow/examples/extension.rs deleted file mode 100644 index 95a64678ef..0000000000 --- a/crates/ferrum-flow/examples/extension.rs +++ /dev/null @@ -1,77 +0,0 @@ -use ferrum_flow::*; -use gpui::{ - AnyElement, AppContext as _, Application, Element as _, ParentElement as _, Styled, - WindowOptions, div, rgb, white, -}; -use serde_json::json; - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("number") - .position(100.0, 100.0) - .size(300.0, 150.0) - .output() - .data(json!({ "label": "Number Node" })) - .build(); - - graph.create_node("").position(300.0, 400.0).input().build(); - - graph - .create_node("undefined") - .position(500.0, 500.0) - .input() - .output() - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .default_plugins() - .plugin(ZoomControlsPlugin::new()) - .plugin(FocusSelectionPlugin::new()) - .plugin(FitAllGraphPlugin::new()) - .plugin(ClipboardPlugin::new()) - .plugin(ContextMenuPlugin::new()) - .node_renderer("number", NumberNode {}) - .build() - }) - }) - .unwrap(); - }); -} - -pub struct NumberNode; - -impl NodeRenderer for NumberNode { - fn render(&self, node: &Node, ctx: &mut RenderContext) -> AnyElement { - let screen = ctx.world_to_screen(node.point()); - let node_x = screen.x; - let node_y = screen.y; - - div() - .absolute() - .left(node_x) - .top(node_y) - .w(ctx.world_length_to_screen(node.size_ref().width)) - .h(ctx.world_length_to_screen(node.size_ref().height)) - .bg(rgb(0x505078)) - .child(div().child("Number Node").text_color(white())) - .into_any() - } - - fn port_render(&self, node: &Node, port: &Port, ctx: &mut RenderContext) -> Option { - let frame = ctx.port_screen_frame(node, port)?; - Some( - frame - .anchor_div() - .rounded_full() - .border_1() - .border_color(rgb(0x1A192B)) - .bg(white()) - .into_any(), - ) - } -} diff --git a/crates/ferrum-flow/examples/plugin.rs b/crates/ferrum-flow/examples/plugin.rs deleted file mode 100644 index 9f53565608..0000000000 --- a/crates/ferrum-flow/examples/plugin.rs +++ /dev/null @@ -1,142 +0,0 @@ -use ferrum_flow::*; -use gpui::{ - AnyElement, AppContext as _, Application, Element as _, ParentElement as _, Styled, - WindowOptions, div, px, rgb, white, -}; -use serde_json::json; - -/// A beginner-friendly custom plugin example. -/// -/// Run with: -/// `cargo run -p ferrum-flow --example plugin` -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("default") - .position(120.0, 120.0) - .input() - .output() - .data(json!({ "label": "Base Node" })) - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .default_plugins() - .plugin(StarterPlugin::new()) - .plugin(ToastPlugin::new()) - .build() - }) - }) - .unwrap(); - }); -} - -/// A tiny plugin that demonstrates: -/// 1) plugin state -/// 2) input handling -/// 3) custom overlay rendering -/// 4) mutating graph data through `PluginContext` -struct StarterPlugin { - next_index: usize, - clicks: usize, - show_hud: bool, -} - -impl StarterPlugin { - fn new() -> Self { - Self { - next_index: 1, - clicks: 0, - show_hud: true, - } - } - - fn add_demo_node(&mut self, ctx: &mut PluginContext) { - let i = self.next_index; - self.next_index += 1; - - let x = 120.0 + ((i % 6) as f32) * 180.0; - let y = 280.0 + ((i / 6) as f32) * 140.0; - - ctx.create_node("default") - .position(x, y) - .input() - .output() - .data(json!({ "label": format!("Plugin Node {i}") })) - .build(); - - ctx.emit(FlowEvent::custom(ToastMessage::success(format!( - "Created node #{i} from StarterPlugin" - )))); - } -} - -impl Plugin for StarterPlugin { - fn name(&self) -> &'static str { - "starter_plugin" - } - - fn setup(&mut self, _ctx: &mut InitPluginContext) { - // Put one initial node index behind the first generated node label. - self.next_index = 1; - } - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let FlowEvent::Input(InputEvent::KeyDown(ev)) = event { - if ev.keystroke.key == "n" { - self.add_demo_node(ctx); - return EventResult::Stop; - } - if ev.keystroke.key == "h" { - self.show_hud = !self.show_hud; - ctx.notify(); - return EventResult::Stop; - } - } - - if let FlowEvent::Input(InputEvent::MouseDown(_)) = event { - self.clicks += 1; - ctx.notify(); - } - - EventResult::Continue - } - - fn render(&mut self, _ctx: &mut RenderContext) -> Option { - if !self.show_hud { - return None; - } - - Some( - div() - .absolute() - .left(px(12.0)) - .top(px(12.0)) - .px_3() - .py_2() - .rounded(px(8.0)) - .bg(rgb(0x001F2937)) - .text_color(white()) - .child(div().text_sm().child("StarterPlugin (custom example)")) - .child( - div() - .text_sm() - .child(format!("Mouse clicks: {}", self.clicks)), - ) - .child(div().text_sm().child("Press N: create node")) - .child(div().text_sm().child("Press H: hide/show this panel")) - .into_any(), - ) - } - - fn priority(&self) -> i32 { - 120 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } -} diff --git a/crates/ferrum-flow/examples/theme.rs b/crates/ferrum-flow/examples/theme.rs deleted file mode 100644 index d2ce87221e..0000000000 --- a/crates/ferrum-flow/examples/theme.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Custom canvas chrome via [`InitPluginContext::theme`] in [`Plugin::setup`]. -use ferrum_flow::*; -use gpui::{AppContext as _, Application, WindowOptions}; -use serde_json::json; - -struct DarkGridThemePlugin; - -impl Plugin for DarkGridThemePlugin { - fn name(&self) -> &'static str { - "dark_grid_theme" - } - - fn setup(&mut self, ctx: &mut InitPluginContext) { - ctx.theme.background = 0x001a1d2a; - ctx.theme.background_grid_dot = 0x003d4559; - ctx.theme.node_card_background = 0x0024283a; - ctx.theme.node_card_border = 0x004a5568; - ctx.theme.node_card_border_selected = 0x00f5a524; - ctx.theme.node_caption_text = 0x00e8eaef; - ctx.theme.default_port_fill = 0x004a5568; - ctx.theme.undefined_node_background = 0x00303845; - ctx.theme.undefined_node_border = 0x00f5a524; - ctx.theme.undefined_node_caption_text = 0x00b8bcc8; - ctx.theme.edge_stroke = 0x0050586b; - ctx.theme.edge_stroke_selected = 0x00f5a524; - ctx.theme.selection_rect_border = 0x006b8cff; - ctx.theme.selection_rect_fill_rgba = 0x6b8cff33; - ctx.theme.port_preview_line = 0x0050586b; - ctx.theme.port_preview_dot = 0x0060809e; - ctx.theme.minimap_background = 0x0018202e; - ctx.theme.minimap_border = 0x004a5568; - ctx.theme.minimap_edge = 0x0050586b; - ctx.theme.minimap_node_fill = 0x0024283a; - ctx.theme.minimap_node_stroke = 0x00607080; - ctx.theme.minimap_viewport_stroke = 0x006b8cff; - ctx.theme.zoom_controls_background = 0x0024283a; - ctx.theme.zoom_controls_border = 0x004a5568; - ctx.theme.zoom_controls_text = 0x00e8eaef; - ctx.theme.context_menu_background = 0x0024283a; - ctx.theme.context_menu_border = 0x004a5568; - ctx.theme.context_menu_text = 0x00e8eaef; - ctx.theme.context_menu_shortcut_text = 0x009098a8; - ctx.theme.context_menu_separator = 0x003d4559; - } -} - -fn main() { - Application::new().run(|cx| { - let mut graph = Graph::new(); - - graph - .create_node("") - .position(100.0, 100.0) - .output() - .output() - .data(json!({ "label": "Themed" })) - .build(); - - cx.open_window(WindowOptions::default(), |window, cx| { - cx.new(|ctx| { - FlowCanvas::builder(graph, ctx, window) - .plugin(DarkGridThemePlugin) - .plugin(MinimapPlugin::new()) - .plugin(SelectionPlugin::new()) - .plugin(NodeInteractionPlugin::new()) - .plugin(ViewportPlugin::new()) - .plugin(ZoomControlsPlugin::new()) - .plugin(BackgroundPlugin::new()) - .plugin(NodePlugin::new()) - .plugin(PortInteractionPlugin::new()) - .plugin(EdgePlugin::new()) - .plugin(ClipboardPlugin::new()) - .plugin(ContextMenuPlugin::new()) - .plugin(SelectAllViewportPlugin::new()) - .plugin(AlignPlugin::new()) - .plugin(FocusSelectionPlugin::new()) - .plugin(FitAllGraphPlugin::new()) - .plugin(DeletePlugin::new()) - .plugin(HistoryPlugin::new()) - .build() - }) - }) - .unwrap(); - }); -} diff --git a/crates/ferrum-flow/src/canvas.rs b/crates/ferrum-flow/src/canvas.rs deleted file mode 100644 index 5717d41523..0000000000 --- a/crates/ferrum-flow/src/canvas.rs +++ /dev/null @@ -1,942 +0,0 @@ -use futures::{StreamExt, channel::mpsc}; -use gpui::*; -use std::cell::Cell; -use std::collections::BTreeMap; -use std::rc::Rc; -use std::time::Duration; - -use crate::{ - BackgroundPlugin, DeletePlugin, EdgePlugin, FlowTheme, GraphChange, HistoryPlugin, - NodeInteractionPlugin, NodePlugin, PortInteractionPlugin, SelectionPlugin, SharedState, - SyncPlugin, SyncPluginContext, ViewportPlugin, - graph::Graph, - plugin::{ - EventResult, FlowEvent, InitPluginContext, InputEvent, Plugin, PluginContext, - PluginRegistry, RenderContext, RenderLayer, invalidate_port_layout_cache_for_graph_change, - }, - viewport::Viewport, -}; - -mod node_renderer; -mod port_cache; -mod types; -mod undo; - -pub use port_cache::PortLayoutCache; - -pub use undo::{Command, CommandContext, CompositeCommand, HistoryProvider, LocalHistory}; - -pub use types::{Interaction, InteractionResult, InteractionState}; - -#[allow(deprecated)] -pub use node_renderer::port_screen_position; -pub use node_renderer::{NodeRenderer, RendererRegistry, default_node_caption}; - -/// Host-side callback for **outbound** [`FlowEvent`]s: invoked synchronously whenever a plugin calls -/// [`PluginContext::emit`](crate::plugin::PluginContext::emit) with the same event that is then -/// enqueued for the internal plugin pipeline ([`FlowCanvas::event_queue`]). -/// -/// # Parent / shell integration -/// -/// This is **not** a GPUI `subscribe` / `observe` stream: you install **one** `FnMut` on the canvas -/// ([`FlowCanvasBuilder::outbound`] or [`FlowCanvas::set_outbound`]). The closure runs on the **UI -/// thread**, **before** the event is pushed onto [`FlowCanvas::event_queue`], and receives a -/// **read-only** reference for inspection ([`FlowEvent::as_custom`]). -/// -/// **Typical patterns** -/// -/// - **Shared counters or queues** — [`std::sync::Arc`] + [`std::sync::atomic::AtomicUsize`] / -/// [`std::sync::Mutex`] / `mpsc` sender; the host view reads them in [`gpui::Render`] (see the -/// `outbound_host` example in this crate). -/// - **Refresh a parent `Entity`** — capture `gpui::Entity` (or a weak handle) in the -/// closure and call [`gpui::Entity::update`] + [`gpui::Context::notify`] after filtering with -/// `as_custom::()`. -/// - **Graph edits without `emit`** — outbound does **not** run for plain [`FlowCanvas::dispatch_command`] -/// unless a plugin later `emit`s; also use [`gpui::Context::observe`] on `Entity` for -/// those cases. -/// -/// ```ignore -/// use ferrum_flow::{FlowCanvas, FlowEvent}; -/// use gpui::{Context, Entity}; -/// -/// fn wire(canvas: &Entity, shell: &Entity, cx: &mut Context) { -/// let shell = shell.clone(); -/// canvas.update(cx, |canvas, cx| { -/// canvas.set_outbound(Some(Box::new(move |ev: &FlowEvent| { -/// if ev.as_custom::().is_some() { -/// let _ = shell.update(cx, |_, cx| cx.notify()); -/// } -/// }))); -/// }); -/// } -/// ``` -pub type FlowCanvasOutbound = Box; - -fn enqueue_plugin_emit( - outbound: &mut Option, - queue: &mut Vec, - e: FlowEvent, -) { - if let Some(h) = outbound.as_mut() { - h(&e); - } - queue.push(e); -} - -pub struct FlowCanvas { - graph: Graph, - - pub(crate) viewport: Viewport, - - pub(crate) plugins_registry: PluginRegistry, - - pub(crate) sync_plugin: Option>, - - renderers: RendererRegistry, - - pub(crate) focus_handle: FocusHandle, - - pub(crate) interaction: InteractionState, - - pub history: Box, - - event_queue: Vec, - port_offset_cache: PortLayoutCache, - - /// Visual tokens for canvas chrome; plugins adjust via [`InitPluginContext::theme`](crate::plugin::InitPluginContext::theme). - theme: FlowTheme, - - /// Type-erased map for cross-plugin data on this canvas instance. - shared_state: SharedState, - canvas_bounds: Rc>>>, - delayed_notify_tx: mpsc::UnboundedSender<()>, - - /// Optional host hook for every plugin [`PluginContext::emit`](crate::plugin::PluginContext::emit). - outbound: Option, -} - -// // TODO -// impl Clone for FlowCanvas { -// fn clone(&self) -> Self { -// Self { -// graph: self.graph.clone(), -// viewport: self.viewport.clone(), -// plugins_registry: PluginRegistry::new(), -// focus_handle: self.focus_handle.clone(), -// interaction: InteractionState::new(), -// event_queue: vec![], -// } -// } -// } - -impl FlowCanvas { - fn init_delayed_notify_channel(&mut self, cx: &mut Context) { - let (tx, mut rx) = mpsc::unbounded::<()>(); - self.delayed_notify_tx = tx; - cx.spawn(async move |this, ctx| { - while rx.next().await.is_some() { - let _ = this.update(ctx, |_, cx| { - cx.notify(); - }); - } - }) - .detach(); - } - - #[deprecated(note = "use builder instead")] - pub fn new(graph: Graph, cx: &mut Context) -> Self { - let focus_handle = cx.focus_handle(); - let (delayed_notify_tx, _rx) = mpsc::unbounded::<()>(); - let mut canvas = Self { - graph, - viewport: Viewport::new(), - plugins_registry: PluginRegistry::new(), - sync_plugin: None, - renderers: RendererRegistry::new(), - focus_handle, - interaction: InteractionState::new(), - history: Box::new(LocalHistory::new()), - event_queue: vec![], - port_offset_cache: PortLayoutCache::new(), - theme: FlowTheme::default(), - shared_state: SharedState::new(), - canvas_bounds: Rc::new(Cell::new(None)), - delayed_notify_tx, - outbound: None, - }; - canvas.init_delayed_notify_channel(cx); - canvas - } - - pub fn builder<'a, 'b>( - graph: Graph, - ctx: &'a mut Context<'b, Self>, - window: &'a Window, - ) -> FlowCanvasBuilder<'a, 'b> { - FlowCanvasBuilder { - graph, - ctx, - window, - plugins: PluginRegistry::new(), - sync_plugin: None, - renderers: RendererRegistry::new(), - theme: FlowTheme::default(), - outbound: None, - } - } - - /// If there is an active [`Interaction`], deliver `MouseMove` / `MouseUp` only to it and return - /// `true` so the plugin chain is skipped for this dispatch (avoids duplicate handling and keeps - /// drag ownership consistent, including for [`Self::process_event_queue`]). - fn dispatch_interaction_pointer(&mut self, event: &FlowEvent, cx: &mut Context) -> bool { - let mut notify = || cx.notify(); - let delayed_notify_tx = self.delayed_notify_tx.clone(); - let mut schedule_after = move |delay: Duration| { - let tx = delayed_notify_tx.clone(); - std::thread::spawn(move || { - std::thread::sleep(delay); - let _ = tx.unbounded_send(()); - }); - }; - match event { - FlowEvent::Input(InputEvent::MouseMove(ev)) => { - let Some(mut handler) = self.interaction.handler.take() else { - return false; - }; - let outbound = &mut self.outbound; - let event_queue = &mut self.event_queue; - let mut emit = |e| enqueue_plugin_emit(outbound, event_queue, e); - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - self.history.as_mut(), - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - let result = handler.on_mouse_move(ev, &mut ctx); - match result { - InteractionResult::Continue => self.interaction.handler = Some(handler), - InteractionResult::End => self.interaction.handler = None, - InteractionResult::Replace(h) => self.interaction.handler = Some(h), - } - true - } - FlowEvent::Input(InputEvent::MouseUp(ev)) => { - let Some(mut handler) = self.interaction.handler.take() else { - return false; - }; - let outbound = &mut self.outbound; - let event_queue = &mut self.event_queue; - let mut emit = |e| enqueue_plugin_emit(outbound, event_queue, e); - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - self.history.as_mut(), - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - let result = handler.on_mouse_up(ev, &mut ctx); - match result { - InteractionResult::Continue => self.interaction.handler = Some(handler), - InteractionResult::End => self.interaction.handler = None, - InteractionResult::Replace(h) => self.interaction.handler = Some(h), - } - true - } - _ => false, - } - } - - fn handle_event(&mut self, event: FlowEvent, cx: &mut Context) { - if let Some(sync_plugin) = &mut self.sync_plugin { - let mut ctx = SyncPluginContext::new(&self.viewport); - sync_plugin.on_event(&event, &mut ctx); - } - - // Pointer stream is owned by the active [`Interaction`]; do not also give Move/Up to plugins. - if self.dispatch_interaction_pointer(&event, cx) { - return; - } - - let outbound = &mut self.outbound; - let event_queue = &mut self.event_queue; - let mut emit = |e| enqueue_plugin_emit(outbound, event_queue, e); - let mut notify = || cx.notify(); - let delayed_notify_tx = self.delayed_notify_tx.clone(); - let mut schedule_after = move |delay: Duration| { - let tx = delayed_notify_tx.clone(); - std::thread::spawn(move || { - std::thread::sleep(delay); - let _ = tx.unbounded_send(()); - }); - }; - - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - self.history.as_mut(), - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - - for plugin in self.plugins_registry.iter_mut() { - let result = plugin.on_event(&event, &mut ctx); - match result { - EventResult::Continue => {} - EventResult::Stop => break, - } - } - } - - /// Same [`PluginContext`] wiring as input dispatch, for **inbound** control from other GPUI - /// entities (toolbar, palette, automation) without touching `graph` directly. - /// - /// Use from `Entity::update`: - /// - /// ```ignore - /// canvas_entity.update(cx, |canvas, cx| { - /// canvas.dispatch_command(CreateNode::new(node), cx); - /// }); - /// ``` - fn with_plugin_context_for_dispatch( - &mut self, - cx: &mut Context, - f: impl FnOnce(&mut PluginContext<'_>), - ) { - let outbound = &mut self.outbound; - let event_queue = &mut self.event_queue; - let mut emit = |e| enqueue_plugin_emit(outbound, event_queue, e); - let mut notify = || cx.notify(); - let delayed_notify_tx = self.delayed_notify_tx.clone(); - let mut schedule_after = move |delay: Duration| { - let tx = delayed_notify_tx.clone(); - std::thread::spawn(move || { - std::thread::sleep(delay); - let _ = tx.unbounded_send(()); - }); - }; - - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - self.history.as_mut(), - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - f(&mut ctx); - } - - /// Run a [`Command`] through the same path as plugins: local [`HistoryProvider`] or - /// [`SyncPlugin::process_intent`], then redraw. - /// - /// Prefer this for graph edits so undo/redo and sync stay consistent. - pub fn dispatch_command(&mut self, command: impl Command + 'static, cx: &mut Context) { - self.with_plugin_context_for_dispatch(cx, |ctx| { - ctx.execute_command(command); - }); - } - - /// Undo the last command (same as plugin [`PluginContext::undo`]). - pub fn dispatch_undo(&mut self, cx: &mut Context) { - self.with_plugin_context_for_dispatch(cx, |ctx| { - ctx.undo(); - }); - } - - /// Redo (same as plugin [`PluginContext::redo`]). - pub fn dispatch_redo(&mut self, cx: &mut Context) { - self.with_plugin_context_for_dispatch(cx, |ctx| { - ctx.redo(); - }); - } - - /// Replace or clear the outbound hook ([`FlowCanvasOutbound`]). Prefer calling from - /// `Entity::update` once the canvas exists; see [`FlowCanvasOutbound`] for parent - /// wiring and the `outbound_host` example in this crate. - /// - /// The hook runs on the same thread as input dispatch, **before** the event is pushed onto - /// [`Self::event_queue`]. Graph changes that do not go through - /// [`PluginContext::emit`](crate::plugin::PluginContext::emit) (for example plain - /// [`Self::dispatch_command`] with no follow-up emit) are **not** reported here; use - /// [`gpui::Context::observe`] on the canvas entity if you need those as well. - pub fn set_outbound(&mut self, hook: Option) { - self.outbound = hook; - } - - /// Read-only view of the document graph (nodes, edges, selection). - pub fn graph(&self) -> &Graph { - &self.graph - } - - /// Clone the graph for use outside the current `update` closure (e.g. async snapshots). - pub fn graph_snapshot(&self) -> Graph { - self.graph.clone() - } - - /// Replace the active canvas visual tokens and request a redraw when they changed. - pub fn set_theme(&mut self, theme: FlowTheme, cx: &mut Context) { - if self.theme == theme { - return; - } - self.theme = theme; - cx.notify(); - } - - /// Replace node renderers and clear cached port layout derived from renderer geometry. - pub fn replace_node_renderers>( - &mut self, - items: impl IntoIterator)>, - cx: &mut Context, - ) { - let mut renderers = RendererRegistry::new(); - for (name, renderer) in items { - renderers.register_boxed(name, renderer); - } - self.renderers = renderers; - self.port_offset_cache.clear_all(); - cx.notify(); - } - - fn process_event_queue(&mut self, cx: &mut Context) { - while let Some(event) = self.event_queue.pop() { - if let Some(sync_plugin) = &mut self.sync_plugin { - let mut ctx = SyncPluginContext::new(&self.viewport); - sync_plugin.on_event(&event, &mut ctx); - } - - if self.dispatch_interaction_pointer(&event, cx) { - continue; - } - - let outbound = &mut self.outbound; - let event_queue = &mut self.event_queue; - let mut emit = |e| enqueue_plugin_emit(outbound, event_queue, e); - let mut notify = || cx.notify(); - let delayed_notify_tx = self.delayed_notify_tx.clone(); - let mut schedule_after = |delay: Duration| { - let tx = delayed_notify_tx.clone(); - std::thread::spawn(move || { - std::thread::sleep(delay); - let _ = tx.unbounded_send(()); - }); - }; - - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - self.history.as_mut(), - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - - for plugin in self.plugins_registry.iter_mut() { - let result = plugin.on_event(&event, &mut ctx); - match result { - EventResult::Continue => {} - EventResult::Stop => break, - } - } - } - } - - fn on_key_down(&mut self, ev: &KeyDownEvent, _: &mut Window, cx: &mut Context) { - self.handle_event(FlowEvent::Input(InputEvent::KeyDown(ev.clone())), cx); - self.process_event_queue(cx); - } - - fn on_key_up(&mut self, ev: &KeyUpEvent, _: &mut Window, cx: &mut Context) { - self.handle_event(FlowEvent::Input(InputEvent::KeyUp(ev.clone())), cx); - self.process_event_queue(cx); - } - - fn on_mouse_down(&mut self, ev: &MouseDownEvent, _: &mut Window, cx: &mut Context) { - self.sync_viewport_to_canvas_bounds(); - let ev = self.mouse_down_event_in_canvas(ev); - self.handle_event(FlowEvent::Input(InputEvent::MouseDown(ev)), cx); - self.process_event_queue(cx); - } - - fn on_mouse_move(&mut self, ev: &MouseMoveEvent, _: &mut Window, cx: &mut Context) { - self.sync_viewport_to_canvas_bounds(); - let ev = self.mouse_move_event_in_canvas(ev); - self.handle_event(FlowEvent::Input(InputEvent::MouseMove(ev)), cx); - self.process_event_queue(cx); - } - - fn on_mouse_up(&mut self, ev: &MouseUpEvent, _: &mut Window, cx: &mut Context) { - self.sync_viewport_to_canvas_bounds(); - let ev = self.mouse_up_event_in_canvas(ev); - self.handle_event(FlowEvent::Input(InputEvent::MouseUp(ev)), cx); - self.process_event_queue(cx); - } - - fn on_scroll_wheel(&mut self, ev: &ScrollWheelEvent, _: &mut Window, cx: &mut Context) { - self.sync_viewport_to_canvas_bounds(); - let ev = self.scroll_wheel_event_in_canvas(ev); - self.handle_event(FlowEvent::Input(InputEvent::Wheel(ev)), cx); - self.process_event_queue(cx); - } - - fn on_canvas_hover(&mut self, hovered: &bool, _: &mut Window, cx: &mut Context) { - self.handle_event(FlowEvent::Input(InputEvent::Hover(*hovered)), cx); - self.process_event_queue(cx); - } - - fn canvas_origin(&self) -> Point { - self.canvas_bounds - .get() - .map(|bounds| bounds.origin) - .unwrap_or(Point::new(px(0.0), px(0.0))) - } - - fn sync_viewport_to_canvas_bounds(&mut self) { - if let Some(bounds) = self.canvas_bounds.get() { - self.viewport.sync_canvas_bounds(bounds); - } - } - - fn mouse_down_event_in_canvas(&self, ev: &MouseDownEvent) -> MouseDownEvent { - let mut ev = ev.clone(); - ev.position = window_point_to_canvas_point(ev.position, self.canvas_origin()); - ev - } - - fn mouse_move_event_in_canvas(&self, ev: &MouseMoveEvent) -> MouseMoveEvent { - let mut ev = ev.clone(); - ev.position = window_point_to_canvas_point(ev.position, self.canvas_origin()); - ev - } - - fn mouse_up_event_in_canvas(&self, ev: &MouseUpEvent) -> MouseUpEvent { - let mut ev = ev.clone(); - ev.position = window_point_to_canvas_point(ev.position, self.canvas_origin()); - ev - } - - fn scroll_wheel_event_in_canvas(&self, ev: &ScrollWheelEvent) -> ScrollWheelEvent { - let mut ev = ev.clone(); - ev.position = window_point_to_canvas_point(ev.position, self.canvas_origin()); - ev - } -} - -impl Render for FlowCanvas { - fn render(&mut self, window: &mut Window, this_cx: &mut Context) -> impl IntoElement { - if let Some(bounds) = self.canvas_bounds.get() { - self.viewport.sync_canvas_bounds(bounds); - } else { - self.viewport.sync_drawable_bounds(window); - } - - let entity = this_cx.entity(); - - let graph = &mut self.graph; - let viewport = &self.viewport; - let renderers = &self.renderers; - let port_offset_cache = &mut self.port_offset_cache; - let theme = &self.theme; - let shared_state = &self.shared_state; - - let mut layers: Vec> = - (0..RenderLayer::ALL.len()).map(|_| Vec::new()).collect(); - - for plugin in self.plugins_registry.iter_mut() { - let mut ctx = RenderContext::new( - graph, - port_offset_cache, - viewport, - renderers, - window, - theme, - shared_state, - ); - - if let Some(el) = plugin.render(&mut ctx) { - layers[plugin.render_layer().index()].push(el); - } - } - - if let Some(i) = self.interaction.handler.as_ref() { - let mut ctx = RenderContext::new( - graph, - port_offset_cache, - viewport, - renderers, - window, - theme, - shared_state, - ); - - if let Some(el) = i.render(&mut ctx) { - layers[RenderLayer::Interaction.index()].push(el); - } - } - - if let Some(sync_plugin) = &mut self.sync_plugin { - let mut ctx = RenderContext::new( - graph, - port_offset_cache, - viewport, - renderers, - window, - theme, - shared_state, - ); - let els = sync_plugin.render(&mut ctx); - for el in els { - layers[RenderLayer::Overlay.index()].push(el); - } - } - - let root = div() - .id("ferrum_flow_canvas") - .size_full() - .track_focus(&self.focus_handle) - .on_key_down(window.listener_for(&entity, Self::on_key_down)) - .on_key_up(window.listener_for(&entity, Self::on_key_up)) - .on_mouse_down( - MouseButton::Left, - window.listener_for(&entity, Self::on_mouse_down), - ) - .on_mouse_down( - MouseButton::Right, - window.listener_for(&entity, Self::on_mouse_down), - ) - .on_mouse_move(window.listener_for(&entity, Self::on_mouse_move)) - .on_hover(window.listener_for(&entity, Self::on_canvas_hover)) - .on_mouse_up( - MouseButton::Left, - window.listener_for(&entity, Self::on_mouse_up), - ) - .on_scroll_wheel(window.listener_for(&entity, Self::on_scroll_wheel)) - .children(RenderLayer::ALL.iter().map(|layer| { - div() - .id(ElementId::Integer(layer.index() as u64)) - .absolute() - .size_full() - .children(layers[layer.index()].drain(..)) - })); - - CanvasRootElement::new(root, Rc::clone(&self.canvas_bounds)) - } -} - -fn window_point_to_canvas_point( - window_point: Point, - canvas_origin: Point, -) -> Point { - window_point - canvas_origin -} - -struct CanvasRootElement { - element: E, - canvas_bounds: Rc>>>, -} - -impl CanvasRootElement { - fn new(element: E, canvas_bounds: Rc>>>) -> Self { - Self { - element, - canvas_bounds, - } - } -} - -impl IntoElement for CanvasRootElement -where - E: Element, -{ - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for CanvasRootElement -where - E: Element, -{ - type RequestLayoutState = E::RequestLayoutState; - type PrepaintState = E::PrepaintState; - - fn id(&self) -> Option { - self.element.id() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - self.element.source_location() - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - self.element.request_layout(id, inspector_id, window, cx) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - self.canvas_bounds.set(Some(bounds)); - self.element - .prepaint(id, inspector_id, bounds, request_layout, window, cx) - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.element.paint( - id, - inspector_id, - bounds, - request_layout, - prepaint, - window, - cx, - ); - } -} - -#[cfg(test)] -mod tests { - use gpui::{Point, px}; - - use super::window_point_to_canvas_point; - - #[test] - fn window_point_to_canvas_point_subtracts_canvas_origin() { - let window_point = Point::new(px(360.0), px(140.0)); - let canvas_origin = Point::new(px(320.0), px(96.0)); - - let canvas_point = window_point_to_canvas_point(window_point, canvas_origin); - - assert_eq!(canvas_point, Point::new(px(40.0), px(44.0))); - } -} - -pub struct FlowCanvasBuilder<'a, 'b> { - graph: Graph, - ctx: &'a mut Context<'b, FlowCanvas>, - window: &'a Window, - - plugins: PluginRegistry, - renderers: RendererRegistry, - sync_plugin: Option>, - theme: FlowTheme, - outbound: Option, -} - -impl<'a, 'b> FlowCanvasBuilder<'a, 'b> { - /// register plugin - pub fn plugin(mut self, plugin: impl Plugin + 'static) -> Self { - self.plugins = self.plugins.add(plugin); - self - } - - /// Registers several plugins in one call (each item is a `Box`). - /// - /// Order is only relevant before [`Self::build`], which sorts by [`Plugin::priority`]. Prefer - /// [`.plugin`](Self::plugin) for single plugins so the compiler boxes them for you. - /// - /// When building a list of heterogeneous plugin types, use an explicitly typed - /// `Vec>` so each `Box::new(concrete)` coerces to the trait object. - pub fn plugins(mut self, plugins: impl IntoIterator>) -> Self { - self.plugins.extend_boxed(plugins); - self - } - - /// Registers the **core** plugin set for editing a node graph on the canvas: background, - /// selection, node drag, pan/zoom, node/edge rendering, port wiring, delete, and undo/redo - /// ([`BackgroundPlugin`], [`SelectionPlugin`], [`NodeInteractionPlugin`], [`ViewportPlugin`], - /// [`NodePlugin`], [`PortInteractionPlugin`], [`EdgePlugin`], [`DeletePlugin`], [`HistoryPlugin`]). - /// - /// Event order is determined by each plugin’s [`Plugin::priority`] when [`FlowCanvas::build`] - /// runs (not by the order of calls to [`.plugin`](Self::plugin)). Add minimap, clipboard, - /// context menu, etc. with [`.plugin`](Self::plugin) before or after this call. - pub fn default_plugins(mut self) -> Self { - self.plugins = self - .plugins - .add(BackgroundPlugin::new()) - .add(SelectionPlugin::new()) - .add(NodeInteractionPlugin::new()) - .add(ViewportPlugin::new()) - .add(NodePlugin::new()) - .add(PortInteractionPlugin::new()) - .add(EdgePlugin::new()) - .add(DeletePlugin::new()) - .add(HistoryPlugin::new()); - self - } - - pub fn sync_plugin(mut self, plugin: impl SyncPlugin + 'static) -> Self { - self.sync_plugin = Some(Box::new(plugin)); - self - } - - /// register node renderer - pub fn node_renderer(mut self, name: impl Into, renderer: R) -> Self - where - R: node_renderer::NodeRenderer + 'static, - { - self.renderers.register(name, renderer); - self - } - - /// Registers several [`NodeRenderer`](node_renderer::NodeRenderer) entries (each `Box`), same idea as [`Self::plugins`]. - pub fn node_renderers>( - mut self, - items: impl IntoIterator)>, - ) -> Self { - for (name, renderer) in items { - self.renderers.register_boxed(name, renderer); - } - self - } - - /// Replace the default [`FlowTheme`] before plugins run [`Plugin::setup`](crate::plugin::Plugin::setup). - pub fn theme(mut self, theme: FlowTheme) -> Self { - self.theme = theme; - self - } - - /// Register an outbound hook: invoked for every [`PluginContext::emit`](crate::plugin::PluginContext::emit) - /// on this canvas (same as [`FlowCanvas::set_outbound`]). See [`FlowCanvasOutbound`] and the - /// `outbound_host` example for how a parent view can react (e.g. `Arc` or - /// `Entity::update` on a shell). - pub fn outbound(mut self, hook: impl FnMut(&FlowEvent) + Send + 'static) -> Self { - self.outbound = Some(Box::new(hook)); - self - } - - pub fn build(self) -> FlowCanvas { - let mut duplicate_plugins: BTreeMap<&'static str, usize> = BTreeMap::new(); - for plugin in self.plugins.iter() { - *duplicate_plugins.entry(plugin.name()).or_insert(0) += 1; - } - for (name, count) in duplicate_plugins - .into_iter() - .filter(|(_, count)| *count > 1) - { - eprintln!( - "warning: plugin '{name}' is registered {count} times; this can cause duplicated event handling" - ); - } - - let focus_handle = self.ctx.focus_handle(); - let drawable_size = self.window.viewport_size(); - let (delayed_notify_tx, _rx) = mpsc::unbounded::<()>(); - - let mut canvas = FlowCanvas { - graph: self.graph, - viewport: Viewport::new(), - plugins_registry: self.plugins, - sync_plugin: self.sync_plugin, - renderers: self.renderers, - focus_handle, - interaction: InteractionState::new(), - history: Box::new(LocalHistory::new()), - event_queue: vec![], - port_offset_cache: PortLayoutCache::new(), - theme: self.theme, - shared_state: SharedState::new(), - canvas_bounds: Rc::new(Cell::new(None)), - delayed_notify_tx, - outbound: self.outbound, - }; - canvas.init_delayed_notify_channel(self.ctx); - - if let Some(sync_plugin) = &mut canvas.sync_plugin { - let (change_sender, mut change_receiver) = mpsc::unbounded::(); - - self.ctx - .spawn(async move |this, ctx| { - while let Some(change) = change_receiver.next().await { - let _ = this.update(ctx, |this, cx| { - invalidate_port_layout_cache_for_graph_change( - &mut this.port_offset_cache, - &this.graph, - &change.kind, - ); - this.graph.apply(change.kind); - cx.notify(); - }); - } - }) - .detach(); - sync_plugin.setup(change_sender); - } - - canvas.plugins_registry.sort_by_priority_desc(); - - { - let mut ctx = InitPluginContext::new( - &mut canvas.graph, - &mut canvas.port_offset_cache, - &mut canvas.viewport, - &mut canvas.renderers, - self.ctx, - drawable_size, - &mut canvas.theme, - &mut canvas.shared_state, - ); - - for plugin in canvas.plugins_registry.iter_mut() { - plugin.setup(&mut ctx); - } - } - - canvas - } -} diff --git a/crates/ferrum-flow/src/canvas/node_renderer.rs b/crates/ferrum-flow/src/canvas/node_renderer.rs deleted file mode 100644 index 6527d66bd9..0000000000 --- a/crates/ferrum-flow/src/canvas/node_renderer.rs +++ /dev/null @@ -1,182 +0,0 @@ -use gpui::*; -use std::collections::HashMap; - -use crate::node::Node; -use crate::plugin::{NodeCardVariant, RenderContext}; -use crate::{Graph, Port, PortId, PortPosition}; - -pub trait NodeRenderer: Send + Sync { - /// render node inner UI - fn render(&self, node: &Node, ctx: &mut RenderContext) -> AnyElement; - - // custom render port UI - fn port_render(&self, node: &Node, port: &Port, ctx: &mut RenderContext) -> Option { - let frame = ctx.port_screen_frame(node, port)?; - Some( - frame - .anchor_div() - .rounded_full() - .bg(rgb(ctx.theme.default_port_fill)) - .into_any(), - ) - } - - /// computing the position of port relative to node - /// built-in Node Plugin is cached this. - fn port_offset(&self, node: &Node, port: &Port, graph: &Graph) -> Point { - let total = graph - .ports_values() - .filter(|p| { - p.node_id() == node.id() - && p.kind() == port.kind() - && p.position() == port.position() - }) - .count() as f32; - let index = port.index() as f32; - let size = *node.size_ref(); - - match port.position() { - PortPosition::Left => { - let spacing = size.height / (total + 1.0); - Point::new(px(0.0), spacing * (index + 1.0)) - } - PortPosition::Right => { - let spacing = size.height / (total + 1.0); - Point::new(size.width, spacing * (index + 1.0)) - } - PortPosition::Top => { - let spacing = size.width / (total + 1.0); - Point::new(spacing * (index + 1.0), px(0.0)) - } - PortPosition::Bottom => { - let spacing = size.width / (total + 1.0); - Point::new(spacing * (index + 1.0), size.height) - } - } - } -} - -pub struct RendererRegistry { - map: HashMap>, - default: Box, - undefined: Box, -} - -impl RendererRegistry { - pub(crate) fn new() -> Self { - Self { - map: HashMap::new(), - default: Box::new(DefaultNodeRenderer {}), - undefined: Box::new(UndefinedNodeRenderer {}), - } - } - - pub fn register(&mut self, name: impl Into, renderer: R) - where - R: NodeRenderer + 'static, - { - self.map.insert(name.into(), Box::new(renderer)); - } - - pub fn register_boxed(&mut self, name: impl Into, renderer: Box) { - self.map.insert(name.into(), renderer); - } - - pub fn get(&self, name: &str) -> &dyn NodeRenderer { - if name.is_empty() || name == "default" { - return self.default.as_ref(); - } - - self.map - .get(name) - .map(|r| r.as_ref()) - .unwrap_or(self.undefined.as_ref()) - } -} - -struct DefaultNodeRenderer; - -impl NodeRenderer for DefaultNodeRenderer { - fn render(&self, node: &Node, ctx: &mut RenderContext) -> AnyElement { - let node_id = node.id(); - let selected = ctx.graph.selected_node().iter().any(|id| *id == node_id); - - ctx.node_card_shell(node, selected, NodeCardVariant::Default) - .rounded(px(6.0)) - .border(px(1.5)) - .child( - div() - .id(ElementId::Uuid(*node_id.as_uuid())) - .size_full() - .flex() - .items_center() - .justify_center() - .text_center() - .px_2() - .child(default_node_caption(node)) - .text_color(rgb(ctx.theme.node_caption_text)), - ) - .into_any() - } -} - -struct UndefinedNodeRenderer; - -impl NodeRenderer for UndefinedNodeRenderer { - fn render(&self, node: &Node, ctx: &mut RenderContext) -> AnyElement { - ctx.node_card_shell(node, false, NodeCardVariant::UndefinedType) - .rounded(px(6.0)) - .border(px(1.5)) - .child( - div() - .id(ElementId::Uuid(*node.id().as_uuid())) - .size_full() - .flex() - .items_center() - .justify_center() - .text_center() - .px_2() - .child(undefined_node_caption(node)) - .text_color(rgb(ctx.theme.undefined_node_caption_text)), - ) - .into_any() - } -} - -#[deprecated(note = "use `ctx.port_screen_center(node, port_id)`")] -pub fn port_screen_position( - node: &Node, - port_id: PortId, - ctx: &RenderContext, -) -> Option> { - ctx.port_screen_center(node, port_id) -} - -fn data_title(data: &serde_json::Value) -> Option { - if let Some(s) = data.get("label").and_then(|v| v.as_str()) { - let t = s.trim(); - if !t.is_empty() { - return Some(t.to_string()); - } - } - None -} - -/// Label for [`DefaultNodeRenderer`]: user-facing title from `data`, else `node_type`, else a generic word. -/// UUID stays off-canvas; use debug/inspector/tooltip if operators need the id. -pub fn default_node_caption(node: &Node) -> String { - if let Some(s) = data_title(node.data_ref()) { - return s; - } - if !node.renderer_key().is_empty() { - return node.renderer_key().to_string(); - } - "Node".to_string() -} - -fn undefined_node_caption(node: &Node) -> String { - if !node.renderer_key().is_empty() { - return format!("Unknown type: {}", node.renderer_key()); - } - "Unknown node type".to_string() -} diff --git a/crates/ferrum-flow/src/canvas/port_cache.rs b/crates/ferrum-flow/src/canvas/port_cache.rs deleted file mode 100644 index 6e5fab8f06..0000000000 --- a/crates/ferrum-flow/src/canvas/port_cache.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::collections::HashMap; - -use gpui::{Pixels, Point}; - -use crate::{EdgeId, Graph, NodeId, PortId, RendererRegistry}; - -#[derive(Debug, Clone)] -pub struct PortLayoutCache { - map: HashMap>>, -} - -impl PortLayoutCache { - pub(crate) fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - pub fn get_offset(&self, node_id: &NodeId, port_id: &PortId) -> Option> { - self.map.get(node_id)?.get(port_id).copied() - } - - pub fn is_node_cached(&self, node_id: &NodeId) -> bool { - self.map.contains_key(node_id) - } - - /// Port ids whose offsets are cached for `node_id` (after [`Self::ensure_node_ports`]). - /// - /// Order follows the inner [`HashMap`] and is not guaranteed stable across runs. - pub fn cached_port_ids_for_node(&self, node_id: &NodeId) -> impl Iterator + '_ { - self.map - .get(node_id) - .into_iter() - .flat_map(|ports| ports.keys().copied()) - } - - pub fn replace_node_offsets( - &mut self, - node_id: NodeId, - offsets: HashMap>, - ) { - self.map.insert(node_id, offsets); - } - - pub fn clear_node(&mut self, node_id: &NodeId) { - self.map.remove(node_id); - } - - pub fn clear_all(&mut self) { - self.map.clear(); - } - - /// Fill port layout for `node_id` if not already cached. - pub fn ensure_node_ports( - &mut self, - graph: &Graph, - renderers: &RendererRegistry, - node_id: &NodeId, - ) { - if self.is_node_cached(node_id) { - return; - } - - let Some(node) = graph.get_node(node_id) else { - return; - }; - - let renderer = renderers.get(node.renderer_key()); - - let mut result = HashMap::new(); - - for port in graph.ports_values().filter(|p| p.node_id() == node.id()) { - let pos = renderer.port_offset(node, port, graph); - result.insert(port.id(), pos); - } - - self.replace_node_offsets(node.id(), result); - } - - /// Fill port layout for every node if not already cached. - pub fn ensure_all_nodes_ports(&mut self, graph: &Graph, renderers: &RendererRegistry) { - let node_ids = graph.nodes().keys().copied(); - - for node_id in node_ids { - self.ensure_node_ports(graph, renderers, &node_id); - } - } - - /// Ensure both endpoint nodes of the edge have port layout cached. - pub fn ensure_edge_ports( - &mut self, - graph: &Graph, - renderers: &RendererRegistry, - edge_id: &EdgeId, - ) { - let Some(edge) = graph.get_edge(edge_id) else { - return; - }; - - self.ensure_node_ports_for_port(graph, renderers, &edge.source_port); - self.ensure_node_ports_for_port(graph, renderers, &edge.target_port); - } - - /// Ensure the node that owns `port_id` has port layout cached. - pub fn ensure_node_ports_for_port( - &mut self, - graph: &Graph, - renderers: &RendererRegistry, - port_id: &PortId, - ) { - let Some(port) = graph.get_port(port_id) else { - return; - }; - - self.ensure_node_ports(graph, renderers, &port.node_id()); - } -} diff --git a/crates/ferrum-flow/src/canvas/types.rs b/crates/ferrum-flow/src/canvas/types.rs deleted file mode 100644 index 6bb68c1ed6..0000000000 --- a/crates/ferrum-flow/src/canvas/types.rs +++ /dev/null @@ -1,51 +0,0 @@ -use gpui::{AnyElement, MouseMoveEvent, MouseUpEvent}; - -use crate::plugin::{PluginContext, RenderContext}; - -pub struct InteractionState { - pub(crate) handler: Option>, -} - -impl InteractionState { - pub(crate) fn new() -> Self { - Self { handler: None } - } - - pub fn add(&mut self, handler: impl Interaction + 'static) { - self.handler = Some(Box::new(handler)); - } - - pub fn clear(&mut self) { - self.handler = None; - } - - pub fn is_some(&self) -> bool { - self.handler.is_some() - } -} - -pub trait Interaction { - fn on_mouse_move( - &mut self, - event: &MouseMoveEvent, - ctx: &mut PluginContext, - ) -> InteractionResult; - - fn on_mouse_up(&mut self, event: &MouseUpEvent, ctx: &mut PluginContext) -> InteractionResult; - - fn render(&self, _ctx: &mut RenderContext) -> Option { - None - } -} - -pub enum InteractionResult { - Continue, - End, - Replace(Box), -} - -impl InteractionResult { - pub fn replace(new_handler: impl Interaction + 'static) -> Self { - Self::Replace(Box::new(new_handler)) - } -} diff --git a/crates/ferrum-flow/src/canvas/undo.rs b/crates/ferrum-flow/src/canvas/undo.rs deleted file mode 100644 index 172583add5..0000000000 --- a/crates/ferrum-flow/src/canvas/undo.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::collections::HashMap; - -use gpui::{Bounds, Pixels, Point}; - -use crate::{ - Edge, EdgeBuilder, EdgeId, Graph, GraphOp, Node, NodeBuilder, NodeId, Port, PortId, - RendererRegistry, SharedState, Viewport, - canvas::PortLayoutCache, - plugin::{is_edge_visible, is_node_visible}, -}; - -pub trait Command { - fn name(&self) -> &'static str; - - /// execute command detail, e.g: move node - fn execute(&mut self, ctx: &mut CommandContext); - - // undo command , when open sync plugin, this is diabeled. - fn undo(&mut self, ctx: &mut CommandContext); - - /// used by sync plugin - /// when open sync plugin, execute method is diasbeld, and using to_ops send graph intent - fn to_ops(&self, _ctx: &mut CommandContext) -> Vec { - vec![] - } -} - -pub trait HistoryProvider { - fn undo(&mut self, ctx: &mut CommandContext); - fn redo(&mut self, ctx: &mut CommandContext); - fn push(&mut self, command: Box, ctx: &mut CommandContext); - fn clear(&mut self); -} - -pub struct CommandContext<'a> { - pub graph: &'a mut Graph, - pub port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - pub renderers: &'a mut RendererRegistry, - /// Shared plugin state on the [`FlowCanvas`](crate::canvas::FlowCanvas). - pub shared_state: &'a mut SharedState, - pub(crate) notify: &'a mut dyn FnMut(), -} -const MAX_HISTORY: usize = 100; -pub struct LocalHistory { - undo_stack: Vec>, - redo_stack: Vec>, -} - -impl LocalHistory { - pub(crate) fn new() -> Self { - Self { - undo_stack: vec![], - redo_stack: vec![], - } - } -} - -impl HistoryProvider for LocalHistory { - fn push(&mut self, mut command: Box, ctx: &mut CommandContext) { - command.execute(ctx); - - self.undo_stack.push(command); - - self.redo_stack.clear(); - - if self.undo_stack.len() > MAX_HISTORY { - self.undo_stack.remove(0); - } - } - fn undo(&mut self, ctx: &mut CommandContext) { - if let Some(mut cmd) = self.undo_stack.pop() { - cmd.undo(ctx); - self.redo_stack.push(cmd); - } - } - - fn redo(&mut self, ctx: &mut CommandContext) { - if let Some(mut cmd) = self.redo_stack.pop() { - cmd.execute(ctx); - self.undo_stack.push(cmd); - } - } - - fn clear(&mut self) { - self.undo_stack.clear(); - self.redo_stack.clear(); - } -} - -pub struct CompositeCommand { - commands: Vec>, -} - -impl Default for CompositeCommand { - fn default() -> Self { - Self::new() - } -} - -impl CompositeCommand { - pub fn new() -> Self { - Self { - commands: Vec::new(), - } - } - pub fn push(&mut self, command: impl Command + 'static) { - self.commands.push(Box::new(command)); - } -} - -impl Command for CompositeCommand { - fn name(&self) -> &'static str { - "composite" - } - fn execute(&mut self, state: &mut CommandContext) { - for cmd in &mut self.commands { - cmd.execute(state); - } - } - - fn undo(&mut self, state: &mut CommandContext) { - for cmd in self.commands.iter_mut().rev() { - cmd.undo(state); - } - } - fn to_ops(&self, ctx: &mut CommandContext) -> Vec { - let mut list = vec![]; - for cmd in &self.commands { - list.extend(cmd.to_ops(ctx)); - } - - vec![GraphOp::Batch(list)] - } -} - -impl<'a> CommandContext<'a> { - pub(crate) fn new( - graph: &'a mut Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - renderers: &'a mut RendererRegistry, - shared_state: &'a mut SharedState, - notify: &'a mut dyn FnMut(), - ) -> Self { - Self { - graph, - port_offset_cache, - viewport, - renderers, - shared_state, - notify, - } - } - pub fn create_node(&mut self, node_type: &str) -> NodeBuilder<'_> { - self.graph.create_node(node_type) - } - - pub fn create_edge(&mut self) -> EdgeBuilder<'_> { - self.graph.create_edge() - } - - pub fn next_node_id(&self) -> NodeId { - self.graph.next_node_id() - } - - pub fn next_port_id(&self) -> PortId { - self.graph.next_port_id() - } - - pub fn next_edge_id(&self) -> EdgeId { - self.graph.next_edge_id() - } - pub fn add_node(&mut self, node: Node) { - self.graph.add_node(node); - } - - pub fn add_port(&mut self, port: Port) { - self.graph.add_port(port); - } - - pub fn remove_port(&mut self, id: &PortId) { - self.graph.remove_port(id); - } - - pub fn get_node(&self, id: &NodeId) -> Option<&Node> { - self.graph.get_node(id) - } - - pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> { - self.graph.get_node_mut(id) - } - pub fn remove_node(&mut self, id: &NodeId) { - self.graph.remove_node(id); - self.port_offset_cache.clear_node(id); - } - pub fn nodes(&self) -> &HashMap { - self.graph.nodes() - } - pub fn node_order(&self) -> &Vec { - self.graph.node_order() - } - - pub fn new_edge(&self) -> Edge { - self.graph.new_edge() - } - - pub fn add_edge(&mut self, edge: Edge) { - self.graph.add_edge(edge); - } - - pub fn remove_edge(&mut self, edge_id: &EdgeId) { - self.graph.remove_edge(edge_id); - } - - pub fn add_selected_node(&mut self, id: NodeId, shift: bool) { - self.graph.add_selected_node(id, shift); - } - pub fn clear_selected_node(&mut self) { - self.graph.clear_selected_node(); - } - pub fn remove_selected_node(&mut self) -> bool { - self.graph.remove_selected_node() - } - - pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) { - self.graph.add_selected_edge(id, shift); - } - pub fn clear_selected_edge(&mut self) { - self.graph.clear_selected_edge(); - } - pub fn remove_selected_edge(&mut self) -> bool { - self.graph.remove_selected_edge() - } - - pub fn selection_bounds(&self) -> Option> { - self.graph.selection_bounds() - } - - pub fn selected_nodes_with_positions(&self) -> HashMap> { - self.graph.selected_nodes_with_positions() - } - - pub fn hit_node(&self, mouse: Point) -> Option { - self.graph.hit_node(mouse, self.viewport) - } - - pub fn bring_node_to_front(&mut self, node_id: NodeId) { - self.graph.bring_node_to_front(node_id); - } - - // ---- Viewport shortcuts ---- - pub fn zoom(&self) -> f32 { - self.viewport.zoom() - } - - pub fn set_zoom(&mut self, zoom: f32) { - self.viewport.set_zoom(zoom); - } - - pub fn zoom_scaled_by(&self, factor: f32) -> f32 { - self.viewport.zoom_scaled_by(factor) - } - - pub fn offset(&self) -> Point { - self.viewport.offset() - } - - pub fn set_offset(&mut self, offset: Point) { - self.viewport.set_offset(offset); - } - - pub fn set_offset_xy(&mut self, x: Pixels, y: Pixels) { - self.viewport.set_offset_xy(x, y); - } - - pub fn translate_offset(&mut self, dx: Pixels, dy: Pixels) { - self.viewport.translate_offset(dx, dy); - } - - pub fn window_bounds(&self) -> Option> { - self.viewport.window_bounds() - } - - pub fn set_window_bounds(&mut self, bounds: Option>) { - self.viewport.set_window_bounds(bounds); - } - - pub fn world_scalar_to_screen(&self, value: f32) -> f32 { - self.viewport.world_scalar_to_screen(value) - } - - pub fn screen_scalar_to_world(&self, value: f32) -> f32 { - self.viewport.screen_scalar_to_world(value) - } - - pub fn world_length_to_screen(&self, value: Pixels) -> Pixels { - self.viewport.world_length_to_screen(value) - } - - pub fn screen_length_to_world(&self, value: Pixels) -> Pixels { - self.viewport.screen_length_to_world(value) - } - - pub fn world_to_screen(&self, p: Point) -> Point { - self.viewport.world_to_screen(p) - } - - pub fn screen_to_world(&self, p: Point) -> Point { - self.viewport.screen_to_world(p) - } - - pub fn is_node_visible(&self, node_id: &NodeId) -> bool { - is_node_visible(self.graph, self.viewport, node_id) - } - pub fn is_node_visible_node(&self, node: &Node) -> bool { - self.viewport.is_node_visible(node) - } - - pub fn is_edge_visible(&self, edge: &Edge) -> bool { - is_edge_visible(self.graph, self.viewport, edge) - } - - pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option> { - self.port_offset_cache.get_offset(node_id, port_id) - } - - pub fn cache_all_node_port_offset(&mut self) { - self.port_offset_cache - .ensure_all_nodes_ports(self.graph, self.renderers); - } - - pub fn cache_node_port_offset(&mut self, node_id: &NodeId) { - self.port_offset_cache - .ensure_node_ports(self.graph, self.renderers, node_id); - } - - pub fn notify(&mut self) { - (self.notify)(); - } -} diff --git a/crates/ferrum-flow/src/command_interop.rs b/crates/ferrum-flow/src/command_interop.rs deleted file mode 100644 index 1af7c3a2a9..0000000000 --- a/crates/ferrum-flow/src/command_interop.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Helpers for verifying that [`Command`](crate::Command) implementations agree between -//! [`Command::execute`](crate::Command::execute), [`Command::undo`](crate::Command::undo), and -//! [`Command::to_ops`](crate::Command::to_ops). -//! -//! Enable the **`testing`** Cargo feature on `ferrum-flow` to use this module: -//! -//! ```toml -//! ferrum-flow = { version = "…", features = ["testing"] } -//! ``` -//! -//! Run this crate’s built-in interop tests with: -//! -//! ```text -//! cargo test -p ferrum-flow --features testing -//! ``` -//! -//! The public entry points are [`graph_snapshot`] and [`assert_command_interop`]. Example tests that -//! use them live next to each [`Command`](crate::Command) implementation under `plugins/` (and -//! `plugins/port/command.rs` for create commands). - -use serde_json::{Value, json}; - -use crate::{ - Command, CommandContext, Graph, GraphOp, RendererRegistry, SharedState, Viewport, - canvas::PortLayoutCache, -}; - -fn with_command_ctx(graph: &mut Graph, f: impl FnOnce(&mut CommandContext) -> R) -> R { - let mut port_offset_cache = PortLayoutCache::new(); - let mut viewport = Viewport::new(); - let mut renderers = RendererRegistry::new(); - let mut shared_state = SharedState::new(); - let mut notify = || {}; - let mut ctx = CommandContext::new( - graph, - &mut port_offset_cache, - &mut viewport, - &mut renderers, - &mut shared_state, - &mut notify, - ); - f(&mut ctx) -} - -fn apply_graph_op(graph: &mut Graph, op: GraphOp) { - match op { - GraphOp::AddNode(node) => { - graph.add_node_without_order(node); - } - GraphOp::RemoveNode { id } => graph.remove_node(&id), - GraphOp::MoveNode { id, x, y } => { - if let Some(node) = graph.get_node_mut(&id) { - node.set_position(x.into(), y.into()); - } - } - GraphOp::ResizeNode { id, size } => { - if let Some(node) = graph.get_node_mut(&id) { - node.set_size_mut(size); - } - } - GraphOp::UpdateNodeData { id, data } => { - if let Some(node) = graph.get_node_mut(&id) { - node.set_data(data); - } - } - GraphOp::NodeOrderInsert { id } => graph.node_order_mut().push(id), - GraphOp::NodeOrderRemove { index } => { - if index < graph.node_order().len() { - graph.node_order_mut().remove(index); - } - } - GraphOp::AddPort(port) => graph.add_port(port), - GraphOp::RemovePort(id) => graph.remove_port(&id), - GraphOp::AddEdge(edge) => graph.add_edge(edge), - GraphOp::RemoveEdge(id) => graph.remove_edge(&id), - GraphOp::Batch(ops) => { - for op in ops { - apply_graph_op(graph, op); - } - } - } -} - -/// Canonical JSON snapshot of a [`Graph`] for stable equality checks (sorted maps / sets). -pub fn graph_snapshot(graph: &Graph) -> Value { - let mut nodes: Vec<_> = graph - .nodes() - .iter() - .map(|(id, n)| { - ( - id.to_string(), - json!({ - "x": f32::from(n.position().0), - "y": f32::from(n.position().1), - "w": f32::from(n.size_ref().width), - "h": f32::from(n.size_ref().height), - "inputs": n.inputs().iter().map(ToString::to_string).collect::>(), - "outputs": n.outputs().iter().map(ToString::to_string).collect::>(), - }), - ) - }) - .collect(); - nodes.sort_by(|a, b| a.0.cmp(&b.0)); - - let mut ports: Vec<_> = graph - .ports() - .iter() - .map(|(id, p)| { - ( - id.to_string(), - json!({ - "node_id": p.node_id().to_string(), - "kind": p.kind().to_string(), - "position": p.position().to_string(), - "index": p.index(), - "w": f32::from(p.size_ref().width), - "h": f32::from(p.size_ref().height), - }), - ) - }) - .collect(); - ports.sort_by(|a, b| a.0.cmp(&b.0)); - - let mut edges: Vec<_> = graph - .edges() - .iter() - .map(|(id, e)| { - ( - id.to_string(), - json!({ - "source": e.source_port.to_string(), - "target": e.target_port.to_string(), - }), - ) - }) - .collect(); - edges.sort_by(|a, b| a.0.cmp(&b.0)); - - let mut selected_node: Vec<_> = graph - .selected_node() - .iter() - .map(ToString::to_string) - .collect(); - selected_node.sort(); - let mut selected_edge: Vec<_> = graph - .selected_edge() - .iter() - .map(ToString::to_string) - .collect(); - selected_edge.sort(); - - json!({ - "nodes": nodes, - "ports": ports, - "edges": edges, - "node_order": graph.node_order().iter().map(ToString::to_string).collect::>(), - "selected_node": selected_node, - "selected_edge": selected_edge - }) -} - -/// Asserts that `execute` + `undo` restores `base`, and that replaying `to_ops` matches `execute`. -pub fn assert_command_interop( - base: &Graph, - mut make: impl FnMut() -> Box, - case_name: &str, -) { - let expected_after_execute = { - let mut g = base.clone(); - with_command_ctx(&mut g, |ctx| { - let mut cmd = make(); - cmd.execute(ctx); - }); - g - }; - - let execute_then_undo = { - let mut g = base.clone(); - with_command_ctx(&mut g, |ctx| { - let mut cmd = make(); - cmd.execute(ctx); - cmd.undo(ctx); - }); - g - }; - assert_eq!( - graph_snapshot(&execute_then_undo), - graph_snapshot(base), - "execute+undo must restore original graph for {case_name}" - ); - - let via_ops = { - let mut g = base.clone(); - with_command_ctx(&mut g, |ctx| { - let cmd = make(); - let ops = cmd.to_ops(ctx); - for op in ops { - apply_graph_op(ctx.graph, op); - } - }); - g - }; - assert_eq!( - graph_snapshot(&via_ops), - graph_snapshot(&expected_after_execute), - "to_ops replay must match execute result for {case_name}" - ); -} diff --git a/crates/ferrum-flow/src/edge.rs b/crates/ferrum-flow/src/edge.rs deleted file mode 100644 index bf5f47d0cb..0000000000 --- a/crates/ferrum-flow/src/edge.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::{fmt::Display, str::FromStr as _}; - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{Graph, PortId}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct EdgeId(Uuid); - -impl Display for EdgeId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for EdgeId { - fn default() -> Self { - Self::new() - } -} - -impl EdgeId { - pub fn new() -> Self { - Self(Uuid::new_v4()) - } - pub fn from_string(s: impl Into) -> Option { - let string = s.into(); - Uuid::from_str(&string).ok().map(Self) - } - pub fn from_uuid(uuid: Uuid) -> Self { - Self(uuid) - } - - pub fn as_uuid(&self) -> &Uuid { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Edge { - pub id: EdgeId, - pub source_port: PortId, - - pub target_port: PortId, -} - -impl Default for Edge { - fn default() -> Self { - Self::new() - } -} - -impl Edge { - pub fn new() -> Self { - Self { - id: EdgeId::new(), - source_port: PortId::new(), - target_port: PortId::new(), - } - } - pub fn source(mut self, port: PortId) -> Self { - self.source_port = port; - self - } - pub fn target(mut self, port: PortId) -> Self { - self.target_port = port; - self - } -} - -pub struct EdgeBuilder<'a> { - graph: Option<&'a mut Graph>, - source: Option, - target: Option, -} - -impl<'a> Default for EdgeBuilder<'a> { - fn default() -> Self { - Self::new() - } -} - -impl<'a> EdgeBuilder<'a> { - pub fn new() -> Self { - Self { - graph: None, - source: None, - target: None, - } - } - - pub fn graph(mut self, graph: &'a mut Graph) -> Self { - self.graph = Some(graph); - self - } - - pub fn source(mut self, port: PortId) -> Self { - self.source = Some(port); - self - } - - pub fn target(mut self, port: PortId) -> Self { - self.target = Some(port); - self - } - - pub fn build(self) -> Option { - let graph = self.graph?; - let source = self.source?; - let target = self.target?; - - let edge_id = graph.next_edge_id(); - - graph.add_edge(Edge { - id: edge_id, - source_port: source, - target_port: target, - }); - - Some(edge_id) - } -} diff --git a/crates/ferrum-flow/src/graph.rs b/crates/ferrum-flow/src/graph.rs deleted file mode 100644 index e75c52fd3e..0000000000 --- a/crates/ferrum-flow/src/graph.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::collections::hash_map::Values as HashMapValues; -use std::collections::hash_set::Iter as HashSetIter; -use std::collections::{HashMap, HashSet}; - -use gpui::{Bounds, Pixels, Point, Size, px}; -use serde::{Deserialize, Serialize}; - -use crate::edge::{Edge, EdgeId}; -use crate::node::{Node, NodeId, Port, PortId}; -use crate::{EdgeBuilder, NodeBuilder, PortKind, PortPosition, Viewport}; - -mod store; - -pub use store::{ChangeSource, GraphChange, GraphChangeKind, GraphOp}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Graph { - nodes: HashMap, - node_order: Vec, - ports: HashMap, - - edges: HashMap, - - selected_edge: HashSet, - selected_node: HashSet, -} - -impl Default for Graph { - fn default() -> Self { - Self::new() - } -} - -impl Graph { - pub fn new() -> Self { - Self { - nodes: HashMap::new(), - node_order: vec![], - ports: HashMap::new(), - edges: HashMap::new(), - selected_edge: HashSet::new(), - selected_node: HashSet::new(), - } - } - - pub fn from_json(json: &str) -> serde_json::Result { - serde_json::from_str(json) - } - - pub fn to_json(&self) -> serde_json::Result { - serde_json::to_string(self) - } - - pub fn is_empty(&self) -> bool { - self.nodes.is_empty() - && self.ports.is_empty() - && self.edges.is_empty() - && self.node_order.is_empty() - } - - pub fn apply(&mut self, op: GraphChangeKind) { - match op { - GraphChangeKind::NodeAdded(node) => self.add_node(node), - GraphChangeKind::NodeRemoved { id } => self.remove_node(&id), - GraphChangeKind::NodeMoved { id, x, y } => { - if let Some(node) = self.nodes.get_mut(&id) { - node.set_position(px(x), px(y)); - } - } - GraphChangeKind::NodeSetWidthed { id, width } => { - if let Some(node) = self.nodes.get_mut(&id) { - node.set_size_width(px(width)); - } - } - GraphChangeKind::NodeSetHeighted { id, height } => { - if let Some(node) = self.nodes.get_mut(&id) { - node.set_size_height(px(height)); - } - } - GraphChangeKind::NodeDataUpdated { id, data } => { - if let Some(node) = self.nodes.get_mut(&id) { - node.set_data(data); - } - } - GraphChangeKind::NodeOrderUpdate(vec) => { - self.node_order = vec; - } - GraphChangeKind::PortAdded(port) => self.add_port(port), - GraphChangeKind::PortRemoved { id } => { - self.remove_port(&id); - } - GraphChangeKind::EdgeAdded(edge) => self.add_edge(edge), - GraphChangeKind::EdgeRemoved { id } => self.remove_edge(&id), - GraphChangeKind::RedrawRequested => {} - GraphChangeKind::Batch(graph_change_kinds) => { - for change in graph_change_kinds { - self.apply(change); - } - } - } - } - - pub fn create_node(&mut self, renderer_key: &str) -> NodeBuilder<'_> { - NodeBuilder::new(renderer_key).graph(self) - } - - pub fn create_edge(&mut self) -> EdgeBuilder<'_> { - EdgeBuilder::new().graph(self) - } - - #[deprecated(note = "use `Graph::create_edge`")] - pub fn create_dege(&mut self) -> EdgeBuilder<'_> { - EdgeBuilder::new().graph(self) - } - - pub fn next_node_id(&self) -> NodeId { - NodeId::new() - } - - pub fn next_port_id(&self) -> PortId { - PortId::new() - } - - pub fn next_edge_id(&self) -> EdgeId { - EdgeId::new() - } - - pub fn add_node(&mut self, node: Node) { - let node_id = node.id(); - self.nodes.insert(node.id(), node); - self.node_order.push(node_id); - } - #[cfg(any(test, feature = "testing"))] - pub(crate) fn add_node_without_order(&mut self, node: Node) { - self.nodes.insert(node.id(), node); - } - - pub fn add_port(&mut self, port: Port) { - let map = &mut self.ports; - map.insert(port.id(), port); - } - - pub fn remove_port(&mut self, id: &PortId) { - self.ports.remove(id); - } - - pub fn nodes(&self) -> &HashMap { - &self.nodes - } - - pub fn node_order(&self) -> &Vec { - &self.node_order - } - pub fn node_order_mut(&mut self) -> &mut Vec { - &mut self.node_order - } - pub fn ports(&self) -> &HashMap { - &self.ports - } - pub fn get_port(&self, id: &PortId) -> Option<&Port> { - self.ports.get(id) - } - pub fn ports_values(&self) -> HashMapValues<'_, PortId, Port> { - self.ports.values() - } - pub fn edges(&self) -> &HashMap { - &self.edges - } - pub fn get_edge(&self, id: &EdgeId) -> Option<&Edge> { - self.edges.get(id) - } - pub fn edges_values(&self) -> HashMapValues<'_, EdgeId, Edge> { - self.edges.values() - } - pub fn selected_node(&self) -> &HashSet { - &self.selected_node - } - pub fn selected_node_is_empty(&self) -> bool { - self.selected_node.is_empty() - } - pub fn selected_node_iter(&self) -> HashSetIter<'_, NodeId> { - self.selected_node.iter() - } - pub fn selected_edge(&self) -> &HashSet { - &self.selected_edge - } - pub fn selected_edge_iter(&self) -> HashSetIter<'_, EdgeId> { - self.selected_edge.iter() - } - pub fn set_selected_node(&mut self, selected: HashSet) { - self.selected_node = selected; - } - pub fn set_selected_edge(&mut self, selected: HashSet) { - self.selected_edge = selected; - } - - pub fn new_edge(&self) -> Edge { - Edge::new() - } - - pub fn add_edge(&mut self, edge: Edge) { - self.edges.insert(edge.id, edge); - } - - pub fn remove_edge(&mut self, edge_id: &EdgeId) { - self.edges.remove(edge_id); - self.selected_edge.remove(edge_id); - } - - pub fn get_node(&self, id: &NodeId) -> Option<&Node> { - self.nodes.get(id) - } - - pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> { - self.nodes.get_mut(id) - } - - pub fn remove_node(&mut self, id: &NodeId) { - let Some(node) = &self.nodes.get(id) else { - return; - }; - - let mut edge_ids_to_remove = HashSet::new(); - for port_id in node.inputs().iter().chain(node.outputs().iter()).copied() { - edge_ids_to_remove.extend( - self.edges - .iter() - .filter(|(_, edge)| edge.source_port == port_id || edge.target_port == port_id) - .map(|(id, _)| *id), - ); - self.ports.remove(&port_id); - } - for edge_id in edge_ids_to_remove { - self.remove_edge(&edge_id); - } - - self.nodes.remove(id); - self.selected_node.remove(id); - let index = self.node_order.iter().position(|v| *v == *id); - if let Some(index) = index { - self.node_order.remove(index); - } - } - - pub fn add_selected_node(&mut self, id: NodeId, shift: bool) { - if shift { - if self.selected_node.contains(&id) { - self.selected_node.remove(&id); - } else { - self.selected_node.insert(id); - } - } else { - self.selected_node.clear(); - self.selected_node.insert(id); - } - } - pub fn clear_selected_node(&mut self) { - self.selected_node.clear(); - } - - pub fn remove_selected_node(&mut self) -> bool { - if self.selected_node.is_empty() { - return false; - } - - let mut ids = vec![]; - for id in self.selected_node.iter() { - ids.push(*id); - } - for id in ids.iter() { - self.remove_node(id); - } - self.selected_node.clear(); - true - } - - pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) { - if shift { - if self.selected_edge.contains(&id) { - self.selected_edge.remove(&id); - } else { - self.selected_edge.insert(id); - } - } else { - self.selected_edge.clear(); - self.selected_edge.insert(id); - } - } - pub fn clear_selected_edge(&mut self) { - self.selected_edge.clear(); - } - - pub fn remove_selected_edge(&mut self) -> bool { - if self.selected_edge.is_empty() { - return false; - } - - let mut ids = vec![]; - for id in self.selected_edge.iter() { - ids.push(*id); - } - for id in ids.iter() { - self.edges.remove(id); - } - self.selected_edge.clear(); - true - } - - pub fn selection_bounds(&self) -> Option> { - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - - let mut found = false; - - for id in &self.selected_node { - let node = &self.nodes.get(id)?; - let (x, y) = node.position(); - let size = *node.size_ref(); - - min_x = min_x.min(x.into()); - min_y = min_y.min(y.into()); - - max_x = max_x.max((x + size.width).into()); - max_y = max_y.max((y + size.height).into()); - - found = true; - } - - if !found { - return None; - } - - Some(Bounds::new( - Point::new(px(min_x), px(min_y)), - Size::new(px(max_x - min_x), px(max_y - min_y)), - )) - } - - pub fn selected_nodes_with_positions(&self) -> HashMap> { - self.selected_node - .iter() - .filter_map(|id| { - let n = &self.nodes.get(id)?; - Some((*id, n.point())) - }) - .collect() - } - - pub fn hit_node(&self, mouse: Point, viewport: &Viewport) -> Option { - self.nodes - .iter() - .filter(|(_, node)| viewport.is_node_visible(node)) - .find(|(_, n)| n.bounds().contains(&mouse)) - .map(|(id, _)| *id) - } - - pub fn bring_node_to_front(&mut self, node_id: NodeId) { - if let Some(index) = self.node_order_mut().iter().position(|id| *id == node_id) { - self.node_order_mut().remove(index); - } - - self.node_order_mut().push(node_id); - } - - pub fn ports_on_node_side( - &self, - node_id: NodeId, - kind: PortKind, - position: PortPosition, - ) -> Vec<&Port> { - self.ports - .values() - .filter(|p| p.node_id() == node_id && p.kind() == kind && p.position() == position) - .collect() - } -} diff --git a/crates/ferrum-flow/src/graph/store.rs b/crates/ferrum-flow/src/graph/store.rs deleted file mode 100644 index c0408cdf6a..0000000000 --- a/crates/ferrum-flow/src/graph/store.rs +++ /dev/null @@ -1,102 +0,0 @@ -use gpui::{Pixels, Size}; -use serde::{Deserialize, Serialize}; - -use crate::{Edge, EdgeId, Node, NodeId, Port, PortId}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[non_exhaustive] -pub enum GraphOp { - // --- Node --- - AddNode(Node), - - RemoveNode { id: NodeId }, - - MoveNode { id: NodeId, x: f32, y: f32 }, - - ResizeNode { id: NodeId, size: Size }, - - UpdateNodeData { id: NodeId, data: serde_json::Value }, - - // --- node_order --- - NodeOrderInsert { id: NodeId }, - NodeOrderRemove { index: usize }, - - // --- Port --- - AddPort(Port), - - RemovePort(PortId), - - // --- Edge --- - AddEdge(Edge), - - RemoveEdge(EdgeId), - - Batch(Vec), -} - -#[derive(Debug, Clone)] -pub struct GraphChange { - pub kind: GraphChangeKind, - pub source: ChangeSource, -} - -impl GraphChange { - pub fn is_local(&self) -> bool { - matches!(self.source, ChangeSource::Local) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ChangeSource { - Local, - Remote, - Undo, - Redo, -} - -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum GraphChangeKind { - // --- Node --- - NodeAdded(Node), - NodeRemoved { - id: NodeId, - }, - NodeMoved { - id: NodeId, - x: f32, - y: f32, - }, - NodeSetWidthed { - id: NodeId, - width: f32, - }, - NodeSetHeighted { - id: NodeId, - height: f32, - }, - NodeDataUpdated { - id: NodeId, - data: serde_json::Value, - }, - - // --- node_order --- - NodeOrderUpdate(Vec), - - // --- Port --- - PortAdded(Port), - PortRemoved { - id: PortId, - }, - - // --- Edge --- - EdgeAdded(Edge), - EdgeRemoved { - id: EdgeId, - }, - - /// No graph mutation; used to request a frame repaint (e.g. after remote awareness updates). - RedrawRequested, - - Batch(Vec), -} diff --git a/crates/ferrum-flow/src/lib.rs b/crates/ferrum-flow/src/lib.rs deleted file mode 100644 index 425b1f7d6b..0000000000 --- a/crates/ferrum-flow/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -mod canvas; -#[cfg(any(feature = "testing", test))] -pub mod command_interop; -mod edge; -mod graph; -mod node; -mod plugin; -#[cfg(any(feature = "testing", test))] -pub mod plugin_testing; -mod plugins; -mod port_screen; -mod shared_state; -mod theme; -mod viewport; - -/// Prefer [`RenderContext::port_screen_frame`](crate::plugin::RenderContext::port_screen_frame). -#[allow(deprecated)] -pub use canvas::port_screen_position; -pub use canvas::{ - Command, CommandContext, CompositeCommand, FlowCanvas, FlowCanvasOutbound, HistoryProvider, - Interaction, InteractionResult, InteractionState, LocalHistory, NodeRenderer, RendererRegistry, - default_node_caption, -}; -pub use edge::*; -pub use graph::*; -pub use node::*; -pub use plugin::{ - EventResult, FlowEvent, InitPluginContext, InputEvent, NodeCardVariant, Plugin, PluginContext, - RenderContext, RenderLayer, SyncPlugin, SyncPluginContext, primary_platform_modifier, -}; -pub use plugins::*; -pub use port_screen::PortScreenFrame; -pub use shared_state::SharedState; -pub use theme::FlowTheme; -pub use viewport::Viewport; diff --git a/crates/ferrum-flow/src/node.rs b/crates/ferrum-flow/src/node.rs deleted file mode 100644 index 674f862cd6..0000000000 --- a/crates/ferrum-flow/src/node.rs +++ /dev/null @@ -1,760 +0,0 @@ -use std::{collections::HashMap, fmt::Display, str::FromStr}; - -use gpui::{Bounds, Pixels, Point, Size, px}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use uuid::Uuid; - -use crate::Graph; - -pub const DEFAULT_NODE_WIDTH: Pixels = px(120.0); -pub const DEFAULT_NODE_HEIGHT: Pixels = px(60.0); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct NodeId(Uuid); - -impl Display for NodeId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for NodeId { - fn default() -> Self { - Self::new() - } -} - -impl NodeId { - pub fn new() -> Self { - Self(Uuid::new_v4()) - } - pub fn from_string(s: impl Into) -> Option { - let string = s.into(); - Uuid::from_str(&string).ok().map(Self) - } - pub fn from_uuid(uuid: Uuid) -> Self { - Self(uuid) - } - - pub fn as_uuid(&self) -> &Uuid { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Node { - // Transitional API: these fields stay public for compatibility in this release. - // Prefer using methods on `Node`; fields will become private in a future release. - #[deprecated(note = "Use `Node::id()` instead; fields will be private in next release.")] - pub id: NodeId, - #[deprecated( - note = "Use `Node::renderer_key()` / `Node::set_renderer_key()` instead; fields will be private in next release." - )] - pub node_type: String, - #[deprecated( - note = "Use `Node::execute_type_ref()` / `Node::set_execute_type()` instead; fields will be private in next release." - )] - pub execute_type: String, - #[deprecated( - note = "Use `Node::position()` / `Node::set_position()` instead; fields will be private in next release." - )] - pub x: Pixels, - #[deprecated( - note = "Use `Node::position()` / `Node::set_position()` instead; fields will be private in next release." - )] - pub y: Pixels, - #[deprecated( - note = "Use `Node::size_ref()` / `Node::set_size_mut()` instead; fields will be private in next release." - )] - pub size: Size, - - #[deprecated( - note = "Use `Node::inputs()` / `Node::push_input()` instead; fields will be private in next release." - )] - pub inputs: Vec, - #[deprecated( - note = "Use `Node::outputs()` / `Node::push_output()` instead; fields will be private in next release." - )] - pub outputs: Vec, - #[deprecated( - note = "Use `Node::data_ref()` / `Node::data_mut()` / `Node::set_data()` instead; fields will be private in next release." - )] - pub data: serde_json::Value, -} - -impl Node { - // Transitional period: `Node` fields are deprecated for external callers, - // but internal constructors/methods still need to read/write those fields. - #[allow(deprecated)] - pub fn new(x: f32, y: f32) -> Self { - Self { - id: NodeId::new(), - node_type: String::new(), - execute_type: String::new(), - x: x.into(), - y: y.into(), - size: Size { - width: DEFAULT_NODE_WIDTH, - height: DEFAULT_NODE_HEIGHT, - }, - inputs: vec![], - outputs: vec![], - data: json!({}), - } - } - - #[allow(deprecated)] - pub fn id(&self) -> NodeId { - self.id - } - - #[allow(deprecated)] - pub(crate) fn set_id(&mut self, id: NodeId) { - self.id = id; - } - - #[allow(deprecated)] - pub fn renderer_key(&self) -> &str { - &self.node_type - } - - #[allow(deprecated)] - pub fn execute_type_ref(&self) -> &str { - &self.execute_type - } - - #[allow(deprecated)] - pub fn set_renderer_key(&mut self, node_type: impl Into) { - self.node_type = node_type.into(); - } - - #[allow(deprecated)] - pub fn set_execute_type(&mut self, execute_type: impl Into) { - self.execute_type = execute_type.into(); - } - - #[allow(deprecated)] - pub fn position(&self) -> (Pixels, Pixels) { - (self.x, self.y) - } - - #[allow(deprecated)] - pub fn position_point(&self) -> Point { - Point::new(self.x, self.y) - } - - #[allow(deprecated)] - pub fn size_ref(&self) -> &Size { - &self.size - } - - #[allow(deprecated)] - pub fn inputs(&self) -> &[PortId] { - &self.inputs - } - - #[allow(deprecated)] - pub fn outputs(&self) -> &[PortId] { - &self.outputs - } - - #[allow(deprecated)] - pub fn data_ref(&self) -> &serde_json::Value { - &self.data - } - - #[allow(deprecated)] - pub fn data_mut(&mut self) -> &mut serde_json::Value { - &mut self.data - } - - #[allow(deprecated)] - pub fn set_position(&mut self, x: Pixels, y: Pixels) { - self.x = x; - self.y = y; - } - - #[allow(deprecated)] - pub fn set_position_with_point(&mut self, point: Point) { - self.x = point.x; - self.y = point.y; - } - - #[allow(deprecated)] - pub fn set_size_mut(&mut self, size: Size) { - self.size = size; - } - - #[allow(deprecated)] - pub fn set_size_width(&mut self, width: Pixels) { - self.size.width = width; - } - - #[allow(deprecated)] - pub fn set_size_height(&mut self, height: Pixels) { - self.size.height = height; - } - - #[allow(deprecated)] - pub fn set_data(&mut self, data: serde_json::Value) { - self.data = data; - } - - #[allow(deprecated)] - pub fn push_input(&mut self, id: PortId) { - self.inputs.push(id); - } - - #[allow(deprecated)] - pub fn push_output(&mut self, id: PortId) { - self.outputs.push(id); - } - - #[allow(deprecated)] - pub fn point(&self) -> Point { - Point::new(self.x, self.y) - } - - #[allow(deprecated)] - pub fn bounds(&self) -> Bounds { - Bounds::new(self.point(), self.size) - } - - #[allow(deprecated)] - pub fn set_size(mut self, size: Size) -> Self { - self.size = size; - self - } - - #[allow(deprecated)] - pub fn output(mut self, id: PortId) -> Self { - self.outputs.push(id); - self - } - - #[allow(deprecated)] - pub fn input(mut self, id: PortId) -> Self { - self.inputs.push(id); - self - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PortId(Uuid); - -impl Display for PortId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for PortId { - fn default() -> Self { - Self::new() - } -} - -impl PortId { - pub fn new() -> Self { - Self(Uuid::new_v4()) - } - pub fn from_string(s: impl Into) -> Option { - let string = s.into(); - Uuid::from_str(&string).ok().map(Self) - } - pub fn from_uuid(uuid: Uuid) -> Self { - Self(uuid) - } - - pub fn as_uuid(&self) -> &Uuid { - &self.0 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum PortKind { - Input, - Output, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub enum PortPosition { - Left, - Right, - Top, - Bottom, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum PortType { - Any, - Bool, - Int, - Float, - String, - List(Box), - Map(Box, Box), - Custom(String), - Union(Vec), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Port { - // Transitional API: these fields stay public for compatibility in this release. - // Prefer using methods on `Port`; fields will become private in a future release. - #[deprecated(note = "Use `Port::id()` instead; fields will be private in next release.")] - pub id: PortId, - #[deprecated(note = "Use `Port::kind()` instead; fields will be private in next release.")] - pub kind: PortKind, - #[deprecated( - note = "Use `Port::index()` / `Port::set_index()` instead; fields will be private in next release." - )] - pub index: usize, - #[deprecated(note = "Use `Port::node_id()` instead; fields will be private in next release.")] - pub node_id: NodeId, - #[deprecated( - note = "Use `Port::position()` / `Port::set_position()` instead; fields will be private in next release." - )] - pub position: PortPosition, - #[deprecated( - note = "Use `Port::size_ref()` / `Port::set_size()` instead; fields will be private in next release." - )] - pub size: Size, - #[deprecated( - note = "Use `Port::port_type_ref()` / `Port::port_type_mut()` instead; fields will be private in next release." - )] - pub port_type: PortType, -} - -impl Port { - #[allow(clippy::too_many_arguments)] - #[allow(deprecated)] - pub fn new( - id: PortId, - kind: PortKind, - index: usize, - node_id: NodeId, - position: PortPosition, - size: Size, - port_type: PortType, - ) -> Self { - Self { - id, - kind, - index, - node_id, - position, - size, - port_type, - } - } - - #[allow(deprecated)] - pub fn id(&self) -> PortId { - self.id - } - - #[allow(deprecated)] - pub fn kind(&self) -> PortKind { - self.kind - } - - #[allow(deprecated)] - pub fn index(&self) -> usize { - self.index - } - - #[allow(deprecated)] - pub fn node_id(&self) -> NodeId { - self.node_id - } - - #[allow(deprecated)] - pub fn position(&self) -> PortPosition { - self.position - } - - #[allow(deprecated)] - pub fn size_ref(&self) -> &Size { - &self.size - } - - #[allow(deprecated)] - pub fn port_type_ref(&self) -> &PortType { - &self.port_type - } - - #[allow(deprecated)] - pub fn port_type_mut(&mut self) -> &mut PortType { - &mut self.port_type - } - - #[allow(deprecated)] - pub fn set_size(&mut self, size: Size) { - self.size = size; - } - - #[allow(deprecated)] - pub fn set_index(&mut self, index: usize) { - self.index = index; - } - - #[allow(deprecated)] - pub fn set_position(&mut self, position: PortPosition) { - self.position = position; - } -} - -impl Display for PortKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PortKind::Input => write!(f, "input"), - PortKind::Output => write!(f, "output"), - } - } -} - -impl Display for PortPosition { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PortPosition::Left => write!(f, "left"), - PortPosition::Right => write!(f, "right"), - PortPosition::Top => write!(f, "top"), - PortPosition::Bottom => write!(f, "bottom"), - } - } -} - -impl FromStr for PortPosition { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "right" => Ok(Self::Right), - "top" => Ok(Self::Top), - "bottom" => Ok(Self::Bottom), - "left" => Ok(Self::Left), - _ => Err(anyhow::anyhow!("Invalid port position: {}", s)), - } - } -} - -pub struct NodeBuilder<'a> { - graph: Option<&'a mut Graph>, - node_type: String, - execute_type: String, - x: Pixels, - y: Pixels, - size: Size, - inputs: Vec, - outputs: Vec, - data: serde_json::Value, -} - -#[derive(Clone)] -pub struct PortSpec { - position: PortPosition, - size: Size, - port_type: PortType, -} - -impl PortSpec { - pub fn input(position: PortPosition) -> Self { - Self { - position, - size: DEFAULT_PORT_SIZE, - port_type: PortType::Any, - } - } - - pub fn output(position: PortPosition) -> Self { - Self { - position, - size: DEFAULT_PORT_SIZE, - port_type: PortType::Any, - } - } - - pub fn with_size(mut self, size: Size) -> Self { - self.size = size; - self - } - - pub fn with_type(mut self, port_type: PortType) -> Self { - self.port_type = port_type; - self - } -} - -const DEFAULT_PORT_SIZE: Size = Size { - width: px(12.0), - height: px(12.0), -}; - -pub struct PortBuilder { - id: PortId, - kind: PortKind, - index: usize, - node_id: NodeId, - position: PortPosition, - size: Size, - port_type: PortType, -} - -impl PortBuilder { - pub fn new(id: PortId) -> Self { - Self { - id, - kind: PortKind::Input, - index: 0, - node_id: NodeId::from_uuid(Uuid::nil()), - position: PortPosition::Left, - size: DEFAULT_PORT_SIZE, - port_type: PortType::Any, - } - } - - pub fn kind(mut self, kind: PortKind) -> Self { - self.kind = kind; - self - } - - pub fn node_id(mut self, node_id: NodeId) -> Self { - self.node_id = node_id; - self - } - - pub fn index(mut self, index: usize) -> Self { - self.index = index; - self - } - - pub fn position(mut self, position: PortPosition) -> Self { - self.position = position; - self - } - - pub fn size(mut self, width: f32, height: f32) -> Self { - self.size = Size::new(px(width), px(height)); - self - } - - pub fn port_type(mut self, port_type: PortType) -> Self { - self.port_type = port_type; - self - } - - pub fn build(self) -> Port { - Port::new( - self.id, - self.kind, - self.index, - self.node_id, - self.position, - self.size, - self.port_type, - ) - } -} - -impl<'a> NodeBuilder<'a> { - pub fn new(renderer_key: impl Into) -> NodeBuilder<'static> { - NodeBuilder { - graph: None, - node_type: renderer_key.into(), - execute_type: String::new(), - x: px(0.0), - y: px(0.0), - size: Size { - width: DEFAULT_NODE_WIDTH, - height: DEFAULT_NODE_HEIGHT, - }, - inputs: vec![], - outputs: vec![], - data: json!({}), - } - } - - pub fn graph(mut self, graph: &'a mut Graph) -> NodeBuilder<'a> { - self.graph = Some(graph); - self - } - - pub fn execute_type(mut self, execute_type: impl Into) -> Self { - self.execute_type = execute_type.into(); - self - } - - pub fn position(mut self, x: f32, y: f32) -> Self { - self.x = x.into(); - self.y = y.into(); - self - } - - pub fn size(mut self, w: f32, h: f32) -> Self { - self.size = Size { - width: w.into(), - height: h.into(), - }; - self - } - - fn push_input_spec(&mut self, spec: PortSpec) { - self.inputs.push(spec); - } - - fn push_output_spec(&mut self, spec: PortSpec) { - self.outputs.push(spec); - } - - pub fn input(mut self) -> Self { - self.push_input_spec(PortSpec::input(PortPosition::Left)); - self - } - - pub fn output(mut self) -> Self { - self.push_output_spec(PortSpec::output(PortPosition::Right)); - self - } - - pub fn input_at(mut self, pos: PortPosition) -> Self { - self.push_input_spec(PortSpec::input(pos)); - self - } - - pub fn output_at(mut self, pos: PortPosition) -> Self { - self.push_output_spec(PortSpec::output(pos)); - self - } - - pub fn input_with(mut self, pos: PortPosition, size: Size) -> Self { - self.push_input_spec(PortSpec::input(pos).with_size(size)); - self - } - - pub fn output_with(mut self, pos: PortPosition, size: Size) -> Self { - self.push_output_spec(PortSpec::output(pos).with_size(size)); - self - } - - pub fn input_port(mut self, spec: PortSpec) -> Self { - self.push_input_spec(spec); - self - } - - pub fn output_port(mut self, spec: PortSpec) -> Self { - self.push_output_spec(spec); - self - } - - pub fn data(mut self, data: serde_json::Value) -> Self { - self.data = data; - self - } - - /// Like [`Self::build_raw`], but uses the given node id and input/output port id lists. - /// Returns an empty port vector: port records are expected to be loaded separately - /// (e.g. from persistence). Any [`PortSpec`]s on this builder are ignored. - #[allow(deprecated)] - pub fn build_raw_with_port_ids( - self, - node_id: NodeId, - input_ids: Vec, - output_ids: Vec, - ) -> Node { - Node { - id: node_id, - node_type: self.node_type, - execute_type: self.execute_type, - x: self.x, - y: self.y, - size: self.size, - inputs: input_ids, - outputs: output_ids, - data: self.data, - } - } - - #[allow(deprecated)] - pub fn build_raw(self) -> (Node, Vec, Option<&'a mut Graph>) { - let node_id = NodeId::new(); - - let mut inputs = Vec::new(); - let mut outputs = Vec::new(); - - let mut input_counters: HashMap = HashMap::new(); - - // Create input ports - let mut ports = vec![]; - for spec in self.inputs { - let port_id = PortId::new(); - - let index = input_counters.entry(spec.position).or_insert(0); - let current_index = *index; - *index += 1; - - ports.push(Port { - id: port_id, - kind: PortKind::Input, - index: current_index, - node_id, - position: spec.position, - size: spec.size, - port_type: spec.port_type, - }); - - inputs.push(port_id); - } - - let mut output_counters: HashMap = HashMap::new(); - - // Create output ports - for spec in self.outputs { - let port_id = PortId::new(); - - let index = output_counters.entry(spec.position).or_insert(0); - let current_index = *index; - *index += 1; - - ports.push(Port { - id: port_id, - kind: PortKind::Output, - index: current_index, - node_id, - position: spec.position, - size: spec.size, - port_type: spec.port_type, - }); - - outputs.push(port_id); - } - - ( - Node { - id: node_id, - node_type: self.node_type, - execute_type: self.execute_type, - x: self.x, - y: self.y, - size: self.size, - inputs, - outputs, - data: self.data, - }, - ports, - self.graph, - ) - } - - pub fn build(self) -> Option { - let (node, ports, graph) = self.build_raw(); - let id = node.id(); - let graph = graph?; - graph.add_node(node); - for port in ports { - graph.add_port(port); - } - Some(id) - } -} diff --git a/crates/ferrum-flow/src/plugin.rs b/crates/ferrum-flow/src/plugin.rs deleted file mode 100644 index af4252083c..0000000000 --- a/crates/ferrum-flow/src/plugin.rs +++ /dev/null @@ -1,1089 +0,0 @@ -use std::{any::Any, collections::HashMap, time::Duration}; - -use gpui::{ - AnyElement, Bounds, Context, Div, ElementId, InteractiveElement as _, KeyDownEvent, KeyUpEvent, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollWheelEvent, Size, Stateful, - Styled, Window, div, rgb, -}; - -use crate::{ - Edge, EdgeBuilder, EdgeId, FlowCanvas, FlowTheme, Graph, GraphOp, Node, NodeBuilder, NodeId, - NodeRenderer, Port, PortId, PortPosition, RendererRegistry, SharedState, Viewport, - canvas::{ - Command, CommandContext, HistoryProvider, Interaction, InteractionState, PortLayoutCache, - }, - port_screen::PortScreenFrame, -}; - -mod sync; -mod utils; - -pub use sync::{SyncPlugin, SyncPluginContext}; - -pub use utils::{ - invalidate_port_layout_cache_for_graph_change, is_edge_visible, is_node_visible, - primary_platform_modifier, -}; - -/// Chrome for [`RenderContext::node_card_shell`]. [`NodeCardVariant::Default`] and -/// [`NodeCardVariant::UndefinedType`] read colors from [`RenderContext::theme`]; plugins may change -/// them via [`InitPluginContext::theme`] / [`PluginContext::theme`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NodeCardVariant { - /// Card from [`FlowTheme::node_card_background`] and border from [`FlowTheme::node_card_border`] - /// / [`FlowTheme::node_card_border_selected`] when `selected`. - Default, - /// Card from [`FlowTheme::undefined_node_background`] and [`FlowTheme::undefined_node_border`] - /// (no selection styling). - UndefinedType, - - /// Geometry and border width only; set `.bg` / `.border_color` yourself. - Custom, -} - -pub trait Plugin { - fn name(&self) -> &'static str; - - fn setup(&mut self, _ctx: &mut InitPluginContext) {} - - fn on_event(&mut self, _event: &FlowEvent, _ctx: &mut PluginContext) -> EventResult { - EventResult::Continue - } - - fn render(&mut self, _ctx: &mut RenderContext) -> Option { - None - } - - fn priority(&self) -> i32 { - 0 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } -} - -pub struct InitPluginContext<'a, 'b> { - graph: &'a mut Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - renderers: &'a mut RendererRegistry, - pub gpui_ctx: &'a Context<'b, FlowCanvas>, - /// Drawable size from the `window` passed to [`FlowCanvas::builder`] (`Window::viewport_size` when `build()` runs). - pub drawable_size: Size, - /// Canvas colors and strokes; mutate in [`Plugin::setup`](Plugin::setup) to customize chrome. - pub theme: &'a mut FlowTheme, - /// Plugin-local shared state on the [`FlowCanvas`](FlowCanvas). - pub shared_state: &'a mut SharedState, - // pub notify: &'a mut dyn FnMut(), -} - -impl<'a, 'b> InitPluginContext<'a, 'b> { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - graph: &'a mut Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - renderers: &'a mut RendererRegistry, - gpui_ctx: &'a Context<'b, FlowCanvas>, - drawable_size: Size, - theme: &'a mut FlowTheme, - shared_state: &'a mut SharedState, - ) -> Self { - Self { - graph, - port_offset_cache, - viewport, - renderers, - gpui_ctx, - drawable_size, - theme, - shared_state, - } - } - pub fn create_node(&mut self, node_type: &str) -> NodeBuilder<'_> { - self.graph.create_node(node_type) - } - - pub fn create_edge(&mut self) -> EdgeBuilder<'_> { - self.graph.create_edge() - } - - pub fn next_node_id(&self) -> NodeId { - self.graph.next_node_id() - } - - pub fn next_port_id(&self) -> PortId { - self.graph.next_port_id() - } - - pub fn next_edge_id(&self) -> EdgeId { - self.graph.next_edge_id() - } - - pub fn add_node(&mut self, node: Node) { - self.graph.add_node(node); - } - - pub fn add_port(&mut self, port: Port) { - self.graph.add_port(port); - } - - pub fn get_node(&self, id: &NodeId) -> Option<&Node> { - self.graph.get_node(id) - } - - pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> { - self.graph.get_node_mut(id) - } - pub fn remove_node(&mut self, id: &NodeId) { - self.graph.remove_node(id); - } - pub fn nodes(&self) -> &HashMap { - self.graph.nodes() - } - pub fn node_order(&self) -> &Vec { - self.graph.node_order() - } - - pub fn new_edge(&self) -> Edge { - self.graph.new_edge() - } - - pub fn add_edge(&mut self, edge: Edge) { - self.graph.add_edge(edge); - } - - pub fn remove_edge(&mut self, edge_id: &EdgeId) { - self.graph.remove_edge(edge_id); - } - - pub fn add_selected_node(&mut self, id: NodeId, shift: bool) { - self.graph.add_selected_node(id, shift); - } - pub fn clear_selected_node(&mut self) { - self.graph.clear_selected_node(); - } - pub fn remove_selected_node(&mut self) -> bool { - self.graph.remove_selected_node() - } - - pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) { - self.graph.add_selected_edge(id, shift); - } - pub fn clear_selected_edge(&mut self) { - self.graph.clear_selected_edge(); - } - pub fn remove_selected_edge(&mut self) -> bool { - self.graph.remove_selected_edge() - } - - pub fn selection_bounds(&self) -> Option> { - self.graph.selection_bounds() - } - - pub fn selected_nodes_with_positions(&self) -> HashMap> { - self.graph.selected_nodes_with_positions() - } - - pub fn hit_node(&self, mouse: Point) -> Option { - self.graph.hit_node(mouse, self.viewport) - } - - pub fn bring_node_to_front(&mut self, node_id: NodeId) { - self.graph.bring_node_to_front(node_id); - } - - // ---- Viewport shortcuts ---- - pub fn zoom(&self) -> f32 { - self.viewport.zoom() - } - - pub fn set_zoom(&mut self, zoom: f32) { - self.viewport.set_zoom(zoom); - } - - pub fn zoom_scaled_by(&self, factor: f32) -> f32 { - self.viewport.zoom_scaled_by(factor) - } - - pub fn offset(&self) -> Point { - self.viewport.offset() - } - - pub fn set_offset(&mut self, offset: Point) { - self.viewport.set_offset(offset); - } - - pub fn set_offset_xy(&mut self, x: Pixels, y: Pixels) { - self.viewport.set_offset_xy(x, y); - } - - pub fn translate_offset(&mut self, dx: Pixels, dy: Pixels) { - self.viewport.translate_offset(dx, dy); - } - - pub fn window_bounds(&self) -> Option> { - self.viewport.window_bounds() - } - - pub fn set_window_bounds(&mut self, bounds: Option>) { - self.viewport.set_window_bounds(bounds); - } - - pub fn world_scalar_to_screen(&self, value: f32) -> f32 { - self.viewport.world_scalar_to_screen(value) - } - - pub fn screen_scalar_to_world(&self, value: f32) -> f32 { - self.viewport.screen_scalar_to_world(value) - } - - pub fn world_length_to_screen(&self, value: Pixels) -> Pixels { - self.viewport.world_length_to_screen(value) - } - - pub fn screen_length_to_world(&self, value: Pixels) -> Pixels { - self.viewport.screen_length_to_world(value) - } - - pub fn world_to_screen(&self, p: Point) -> Point { - self.viewport.world_to_screen(p) - } - - pub fn screen_to_world(&self, p: Point) -> Point { - self.viewport.screen_to_world(p) - } - - pub fn edge_control_point( - &self, - source: Point, - position: PortPosition, - ) -> Point { - self.viewport.edge_control_point(source, position) - } - - pub fn is_node_visible(&self, node_id: &NodeId) -> bool { - is_node_visible(self.graph, self.viewport, node_id) - } - pub fn is_node_visible_node(&self, node: &Node) -> bool { - self.viewport.is_node_visible(node) - } - - pub fn is_edge_visible(&self, edge: &Edge) -> bool { - is_edge_visible(self.graph, self.viewport, edge) - } - - pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option> { - self.port_offset_cache.get_offset(node_id, port_id) - } - - /// Port center in screen pixels when you already have the owning [`Node`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center(&self, node: &Node, port_id: PortId) -> Option> { - let node_pos = node.point(); - let offset = self.port_offset_cached(&node.id(), &port_id)?; - Some(self.viewport.world_to_screen(node_pos + offset)) - } - - /// Like [`Self::port_screen_center`], resolving the port from [`Graph::ports`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center_by_port_id(&self, port_id: PortId) -> Option> { - let port = self.graph.get_port(&port_id)?; - let node = self.get_node(&port.node_id())?; - self.port_screen_center(node, port_id) - } - - /// Full port layout for custom [`NodeRenderer::port_render`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_frame(&self, node: &Node, port: &Port) -> Option { - Some(PortScreenFrame { - center: self.port_screen_center(node, port.id())?, - size: *port.size_ref(), - zoom: self.viewport.zoom(), - port_id: port.id(), - }) - } - - pub fn cache_port_offset_with_node(&mut self, node_ids: &Vec) { - for node_id in node_ids { - self.cache_node_port_offset(node_id); - } - } - - pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) { - self.port_offset_cache - .ensure_edge_ports(self.graph, self.renderers, edge_id); - } - - pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) { - self.port_offset_cache - .ensure_node_ports_for_port(self.graph, self.renderers, port_id); - } - - fn cache_node_port_offset(&mut self, node_id: &NodeId) { - self.port_offset_cache - .ensure_node_ports(self.graph, self.renderers, node_id); - } -} - -pub struct PluginContext<'a> { - pub graph: &'a mut Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - pub(crate) interaction: &'a mut InteractionState, - renderers: &'a mut RendererRegistry, - - sync_plugin: &'a mut Option>, - - history: &'a mut dyn HistoryProvider, - /// Canvas theme; change during event handling and call [`PluginContext::notify`] to redraw. - pub theme: &'a mut FlowTheme, - /// Plugin-local shared state on the [`FlowCanvas`](FlowCanvas). - pub shared_state: &'a mut SharedState, - emit: &'a mut dyn FnMut(FlowEvent), - notify: &'a mut dyn FnMut(), - schedule_after: &'a mut dyn FnMut(Duration), -} - -pub enum EventResult { - Continue, - Stop, -} - -impl<'a> PluginContext<'a> { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - graph: &'a mut Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a mut Viewport, - interaction: &'a mut InteractionState, - renderers: &'a mut RendererRegistry, - sync_plugin: &'a mut Option>, - history: &'a mut dyn HistoryProvider, - theme: &'a mut FlowTheme, - shared_state: &'a mut SharedState, - emit: &'a mut dyn FnMut(FlowEvent), - notify: &'a mut dyn FnMut(), - schedule_after: &'a mut dyn FnMut(Duration), - ) -> Self { - Self { - graph, - port_offset_cache, - viewport, - interaction, - renderers, - sync_plugin, - history, - theme, - shared_state, - emit, - notify, - schedule_after, - } - } - - pub fn start_interaction(&mut self, handler: impl Interaction + 'static) { - self.interaction.handler = Some(Box::new(handler)); - } - - pub fn cancel_interaction(&mut self) { - self.interaction.handler = None; - } - - pub fn has_interaction(&self) -> bool { - self.interaction.handler.is_some() - } - - /// Tell GPUI that this entity has changed and observers of it should be notified. - pub fn notify(&mut self) { - (self.notify)(); - } - - /// Schedule a future canvas refresh after a delay. - pub fn schedule_after(&mut self, delay: Duration) { - (self.schedule_after)(delay); - } - - /// Enqueue a follow-up event for plugins and notify the canvas. If the host registered - /// [`FlowCanvas::set_outbound`](crate::canvas::FlowCanvas::set_outbound) or - /// [`FlowCanvasBuilder::outbound`](crate::canvas::FlowCanvasBuilder::outbound), the same - /// `event` is passed there first (synchronous, read-only). - pub fn emit(&mut self, event: FlowEvent) { - (self.emit)(event); - self.notify(); - } - - pub fn has_sync_plugin(&self) -> bool { - self.sync_plugin.is_some() - } - - pub fn execute_command(&mut self, command: impl Command + 'static) { - let mut ctx = CommandContext::new( - self.graph, - self.port_offset_cache, - self.viewport, - self.renderers, - self.shared_state, - self.notify, - ); - if let Some(sync) = &mut self.sync_plugin { - sync.process_intent(GraphOp::Batch(command.to_ops(&mut ctx))); - - self.notify(); - } else { - self.history.push(Box::new(command), &mut ctx); - - self.notify(); - } - } - - pub fn undo(&mut self) { - if let Some(sync) = &mut self.sync_plugin { - sync.undo(); - } else { - let mut ctx = CommandContext::new( - self.graph, - self.port_offset_cache, - self.viewport, - self.renderers, - self.shared_state, - self.notify, - ); - - self.history.undo(&mut ctx); - - self.notify(); - } - } - - pub fn redo(&mut self) { - if let Some(sync) = &mut self.sync_plugin { - sync.redo(); - } else { - let mut ctx = CommandContext::new( - self.graph, - self.port_offset_cache, - self.viewport, - self.renderers, - self.shared_state, - self.notify, - ); - - self.history.redo(&mut ctx); - - self.notify(); - } - } - - pub fn history_clear(&mut self) { - self.history.clear(); - } - - pub fn create_node(&mut self, node_type: &str) -> NodeBuilder<'_> { - self.graph.create_node(node_type) - } - - pub fn create_edge(&mut self) -> EdgeBuilder<'_> { - self.graph.create_edge() - } - - pub fn next_node_id(&self) -> NodeId { - self.graph.next_node_id() - } - - pub fn next_port_id(&self) -> PortId { - self.graph.next_port_id() - } - - pub fn next_edge_id(&self) -> EdgeId { - self.graph.next_edge_id() - } - - pub fn add_node(&mut self, node: Node) { - self.graph.add_node(node); - } - - pub fn add_port(&mut self, port: Port) { - self.graph.add_port(port); - } - - pub fn get_node(&self, id: &NodeId) -> Option<&Node> { - self.graph.get_node(id) - } - - pub fn get_node_render(&self, id: &NodeId) -> Option<&dyn NodeRenderer> { - let node = self.get_node(id)?; - - Some(self.renderers.get(node.renderer_key())) - } - - /// World-space offset from the node's top-left ([`Node::point`]) to the port anchor used for - /// edge wiring (same as [`NodeRenderer::port_offset`]). - /// - /// `graph` must contain `node` and that node's ports (a scratch graph is fine) so multi-port - /// spacing matches runtime layout. - pub(crate) fn port_world_offset_relative( - &self, - graph: &Graph, - node: &Node, - port: &Port, - ) -> Point { - self.renderers - .get(node.renderer_key()) - .port_offset(node, port, graph) - } - - pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> { - self.graph.get_node_mut(id) - } - pub fn remove_node(&mut self, id: &NodeId) { - self.graph.remove_node(id); - self.port_offset_cache.clear_node(id); - } - pub fn nodes(&self) -> &HashMap { - self.graph.nodes() - } - pub fn node_order(&self) -> &Vec { - self.graph.node_order() - } - - pub fn new_edge(&self) -> Edge { - self.graph.new_edge() - } - - pub fn add_edge(&mut self, edge: Edge) { - self.graph.add_edge(edge); - } - - pub fn remove_edge(&mut self, edge_id: &EdgeId) { - self.graph.remove_edge(edge_id); - } - - pub fn add_selected_node(&mut self, id: NodeId, shift: bool) { - self.graph.add_selected_node(id, shift); - } - pub fn clear_selected_node(&mut self) { - self.graph.clear_selected_node(); - } - pub fn remove_selected_node(&mut self) -> bool { - self.graph.remove_selected_node() - } - - pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) { - self.graph.add_selected_edge(id, shift); - } - pub fn clear_selected_edge(&mut self) { - self.graph.clear_selected_edge(); - } - pub fn remove_selected_edge(&mut self) -> bool { - self.graph.remove_selected_edge() - } - - pub fn selection_bounds(&self) -> Option> { - self.graph.selection_bounds() - } - - pub fn selected_nodes_with_positions(&self) -> HashMap> { - self.graph.selected_nodes_with_positions() - } - - pub fn hit_node(&self, mouse: Point) -> Option { - self.graph.hit_node(mouse, self.viewport) - } - - pub fn bring_node_to_front(&mut self, node_id: NodeId) { - self.graph.bring_node_to_front(node_id); - } - - // ---- Viewport shortcuts ---- - pub fn zoom(&self) -> f32 { - self.viewport.zoom() - } - - pub fn set_zoom(&mut self, zoom: f32) { - self.viewport.set_zoom(zoom); - } - - pub fn zoom_scaled_by(&self, factor: f32) -> f32 { - self.viewport.zoom_scaled_by(factor) - } - - pub fn offset(&self) -> Point { - self.viewport.offset() - } - - pub fn set_offset(&mut self, offset: Point) { - self.viewport.set_offset(offset); - } - - pub fn set_offset_xy(&mut self, x: Pixels, y: Pixels) { - self.viewport.set_offset_xy(x, y); - } - - pub fn translate_offset(&mut self, dx: Pixels, dy: Pixels) { - self.viewport.translate_offset(dx, dy); - } - - pub fn window_bounds(&self) -> Option> { - self.viewport.window_bounds() - } - - pub fn set_window_bounds(&mut self, bounds: Option>) { - self.viewport.set_window_bounds(bounds); - } - - pub fn world_scalar_to_screen(&self, value: f32) -> f32 { - self.viewport.world_scalar_to_screen(value) - } - - pub fn screen_scalar_to_world(&self, value: f32) -> f32 { - self.viewport.screen_scalar_to_world(value) - } - - pub fn world_length_to_screen(&self, value: Pixels) -> Pixels { - self.viewport.world_length_to_screen(value) - } - - pub fn screen_length_to_world(&self, value: Pixels) -> Pixels { - self.viewport.screen_length_to_world(value) - } - - pub fn world_to_screen(&self, p: Point) -> Point { - self.viewport.world_to_screen(p) - } - - pub fn screen_to_world(&self, p: Point) -> Point { - self.viewport.screen_to_world(p) - } - - pub fn edge_control_point( - &self, - source: Point, - position: PortPosition, - ) -> Point { - self.viewport.edge_control_point(source, position) - } - - pub fn is_node_visible(&self, node_id: &NodeId) -> bool { - is_node_visible(self.graph, self.viewport, node_id) - } - pub fn is_node_visible_node(&self, node: &Node) -> bool { - self.viewport.is_node_visible(node) - } - - pub fn is_edge_visible(&self, edge: &Edge) -> bool { - is_edge_visible(self.graph, self.viewport, edge) - } - - pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option> { - self.port_offset_cache.get_offset(node_id, port_id) - } - - pub fn port_offset_cache_clear_all(&mut self) { - self.port_offset_cache.clear_all(); - } - - /// Port center in screen pixels when you already have the owning [`Node`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center(&self, node: &Node, port_id: PortId) -> Option> { - let node_pos = node.point(); - let offset = self.port_offset_cached(&node.id(), &port_id)?; - Some(self.viewport.world_to_screen(node_pos + offset)) - } - - /// Like [`Self::port_screen_center`], resolving the port from [`Graph::ports`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center_by_port_id(&self, port_id: PortId) -> Option> { - let port = self.graph.get_port(&port_id)?; - let node = self.get_node(&port.node_id())?; - self.port_screen_center(node, port_id) - } - - /// Full port layout for custom [`NodeRenderer::port_render`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_frame(&self, node: &Node, port: &Port) -> Option { - Some(PortScreenFrame { - center: self.port_screen_center(node, port.id())?, - size: *port.size_ref(), - zoom: self.viewport.zoom(), - port_id: port.id(), - }) - } - - pub fn cache_all_node_port_offset(&mut self) { - self.port_offset_cache - .ensure_all_nodes_ports(self.graph, self.renderers); - } - - pub fn cache_port_offset_with_node(&mut self, node_ids: &Vec) { - for node_id in node_ids { - self.cache_node_port_offset(node_id); - } - } - - pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) { - self.port_offset_cache - .ensure_edge_ports(self.graph, self.renderers, edge_id); - } - - pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) { - self.port_offset_cache - .ensure_node_ports_for_port(self.graph, self.renderers, port_id); - } - - fn cache_node_port_offset(&mut self, node_id: &NodeId) { - self.port_offset_cache - .ensure_node_ports(self.graph, self.renderers, node_id); - } -} - -pub enum FlowEvent { - Input(InputEvent), - Custom(Box), -} - -impl FlowEvent { - pub fn custom(event: T) -> Self { - FlowEvent::Custom(Box::new(event)) - } - pub fn as_custom(&self) -> Option<&T> { - match self { - FlowEvent::Custom(e) => e.downcast_ref::(), - _ => None, - } - } -} - -pub enum InputEvent { - KeyDown(KeyDownEvent), - KeyUp(KeyUpEvent), - - MouseDown(MouseDownEvent), - MouseMove(MouseMoveEvent), - MouseUp(MouseUpEvent), - - Wheel(ScrollWheelEvent), - - Hover(bool), -} - -pub struct RenderContext<'a> { - pub graph: &'a Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a Viewport, - pub renderers: &'a RendererRegistry, - - pub window: &'a Window, - /// Active canvas theme (from [`FlowCanvas::theme`](crate::canvas::FlowCanvas::theme)). - pub theme: &'a FlowTheme, - /// Read-only shared plugin state on the [`FlowCanvas`](FlowCanvas). - shared_state: &'a SharedState, -} - -impl<'a> RenderContext<'a> { - pub(crate) fn new( - graph: &'a Graph, - port_offset_cache: &'a mut PortLayoutCache, - viewport: &'a Viewport, - renderers: &'a RendererRegistry, - window: &'a Window, - theme: &'a FlowTheme, - shared_state: &'a SharedState, - ) -> Self { - Self { - graph, - port_offset_cache, - viewport, - renderers, - window, - theme, - shared_state, - } - } - - /// Detached builder (no graph); use [`PluginContext::create_node`] or [`Graph::create_node`] to commit. - pub fn create_node(&self, renderer_key: &str) -> NodeBuilder<'_> { - NodeBuilder::new(renderer_key) - } - - pub fn next_node_id(&self) -> NodeId { - self.graph.next_node_id() - } - - pub fn next_port_id(&self) -> PortId { - self.graph.next_port_id() - } - - pub fn next_edge_id(&self) -> EdgeId { - self.graph.next_edge_id() - } - - pub fn get_node(&self, id: &NodeId) -> Option<&Node> { - self.graph.get_node(id) - } - - pub fn get_node_render(&self, id: &NodeId) -> Option<&dyn NodeRenderer> { - let node = self.get_node(id)?; - - Some(self.renderers.get(node.renderer_key())) - } - - pub fn nodes(&self) -> &HashMap { - self.graph.nodes() - } - pub fn node_order(&self) -> &Vec { - self.graph.node_order() - } - - pub fn new_edge(&self) -> Edge { - self.graph.new_edge() - } - - pub fn selection_bounds(&self) -> Option> { - self.graph.selection_bounds() - } - - pub fn selected_nodes_with_positions(&self) -> HashMap> { - self.graph.selected_nodes_with_positions() - } - - pub fn hit_node(&self, mouse: Point) -> Option { - self.graph.hit_node(mouse, self.viewport) - } - - // ---- Viewport shortcuts ---- - - pub fn viewport(&self) -> &Viewport { - self.viewport - } - - pub fn zoom(&self) -> f32 { - self.viewport.zoom() - } - - pub fn zoom_scaled_by(&self, factor: f32) -> f32 { - self.viewport.zoom_scaled_by(factor) - } - - pub fn offset(&self) -> Point { - self.viewport.offset() - } - - pub fn window_bounds(&self) -> Option> { - self.viewport.window_bounds() - } - - pub fn world_scalar_to_screen(&self, value: f32) -> f32 { - self.viewport.world_scalar_to_screen(value) - } - - pub fn screen_scalar_to_world(&self, value: f32) -> f32 { - self.viewport.screen_scalar_to_world(value) - } - - pub fn world_length_to_screen(&self, value: Pixels) -> Pixels { - self.viewport.world_length_to_screen(value) - } - - pub fn screen_length_to_world(&self, value: Pixels) -> Pixels { - self.viewport.screen_length_to_world(value) - } - - pub fn world_to_screen(&self, p: Point) -> Point { - self.viewport.world_to_screen(p) - } - - /// Absolute-positioned node card shell: screen origin, zoom-scaled size. - /// - /// Chain `.child(...)` for the inner body, then `.into_any()` (see [`gpui::Element`]). - pub fn node_card_shell( - &self, - node: &Node, - selected: bool, - variant: NodeCardVariant, - ) -> Stateful
{ - let screen = self.world_to_screen(node.point()); - let z = self.viewport.zoom(); - let base = div() - .id(ElementId::Uuid(*node.id().as_uuid())) - .absolute() - .left(screen.x) - .top(screen.y) - .w(node.size_ref().width * z) - .h(node.size_ref().height * z); - let t = self.theme; - match variant { - NodeCardVariant::Default => { - base.bg(rgb(t.node_card_background)) - .border_color(rgb(if selected { - t.node_card_border_selected - } else { - t.node_card_border - })) - } - NodeCardVariant::UndefinedType => base - .bg(rgb(t.undefined_node_background)) - .border_color(rgb(t.undefined_node_border)), - NodeCardVariant::Custom => base, - } - } - - pub fn screen_to_world(&self, p: Point) -> Point { - self.viewport.screen_to_world(p) - } - - pub fn edge_control_point( - &self, - source: Point, - position: PortPosition, - ) -> Point { - self.viewport.edge_control_point(source, position) - } - - pub fn is_node_visible(&self, node_id: &NodeId) -> bool { - is_node_visible(self.graph, self.viewport, node_id) - } - pub fn is_node_visible_node(&self, node: &Node) -> bool { - self.viewport.is_node_visible(node) - } - - pub fn is_edge_visible(&self, edge: &Edge) -> bool { - is_edge_visible(self.graph, self.viewport, edge) - } - - pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option> { - self.port_offset_cache.get_offset(node_id, port_id) - } - - /// Port ids with layout cached for this node (see [`PortLayoutCache::cached_port_ids_for_node`]). - /// - /// Call [`Self::cache_port_offset_with_nodes`] (or other `cache_port_offset_*` helpers) first - /// so the list is complete for rendering. - pub fn cached_port_ids_for_node(&self, node_id: &NodeId) -> impl Iterator + '_ { - self.port_offset_cache.cached_port_ids_for_node(node_id) - } - - /// Port center in screen pixels when you already have the owning [`Node`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center(&self, node: &Node, port_id: PortId) -> Option> { - let node_pos = node.point(); - let offset = self.port_offset_cached(&node.id(), &port_id)?; - Some(self.viewport.world_to_screen(node_pos + offset)) - } - - /// Like [`Self::port_screen_center`], resolving the port from [`Graph::ports`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_center_by_port_id(&self, port_id: PortId) -> Option> { - let port = self.graph.get_port(&port_id)?; - let node = self.get_node(&port.node_id())?; - self.port_screen_center(node, port_id) - } - - /// Full port layout for custom [`NodeRenderer::port_render`]. - /// *warning*: this is using port offset cache, so it will not be accurate if the port offset is not cached. - pub fn port_screen_frame(&self, node: &Node, port: &Port) -> Option { - Some(PortScreenFrame { - center: self.port_screen_center(node, port.id())?, - size: *port.size_ref(), - zoom: self.viewport.zoom(), - port_id: port.id(), - }) - } - - pub fn cache_all_node_port_offset(&mut self) { - self.port_offset_cache - .ensure_all_nodes_ports(self.graph, self.renderers); - } - - pub fn cache_port_offset_with_nodes(&mut self, node_ids: &[NodeId]) { - for node_id in node_ids { - self.cache_node_port_offset(node_id); - } - } - - pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) { - self.port_offset_cache - .ensure_edge_ports(self.graph, self.renderers, edge_id); - } - - pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) { - self.port_offset_cache - .ensure_node_ports_for_port(self.graph, self.renderers, port_id); - } - - fn cache_node_port_offset(&mut self, node_id: &NodeId) { - self.port_offset_cache - .ensure_node_ports(self.graph, self.renderers, node_id); - } - - pub fn get_shared_state(&self) -> Option<&T> { - self.shared_state.get::() - } - - pub fn contains_shared_state(&self) -> bool { - self.shared_state.contains::() - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum RenderLayer { - Background, - Edges, - Nodes, - Selection, - Interaction, - Overlay, -} - -impl RenderLayer { - pub const ALL: [RenderLayer; 6] = [ - RenderLayer::Background, - RenderLayer::Edges, - RenderLayer::Nodes, - RenderLayer::Selection, - RenderLayer::Interaction, - RenderLayer::Overlay, - ]; - pub fn index(self) -> usize { - match self { - RenderLayer::Background => 0, - RenderLayer::Edges => 1, - RenderLayer::Nodes => 2, - RenderLayer::Selection => 3, - RenderLayer::Interaction => 4, - RenderLayer::Overlay => 5, - } - } -} - -pub struct PluginRegistry { - plugins: Vec>, -} - -impl PluginRegistry { - pub(crate) fn new() -> Self { - Self { plugins: vec![] } - } - - pub fn add(mut self, plugin: impl Plugin + 'static) -> Self { - self.plugins.push(Box::new(plugin)); - self - } - - pub fn extend_boxed(&mut self, plugins: impl IntoIterator>) { - self.plugins.extend(plugins); - } - - pub fn sort_by_priority_desc(&mut self) { - self.plugins.sort_by_key(|p| -p.priority()); - } - - pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Box> { - self.plugins.iter_mut() - } - - pub fn iter(&self) -> std::slice::Iter<'_, Box> { - self.plugins.iter() - } -} diff --git a/crates/ferrum-flow/src/plugin/sync.rs b/crates/ferrum-flow/src/plugin/sync.rs deleted file mode 100644 index 4d455bbdd5..0000000000 --- a/crates/ferrum-flow/src/plugin/sync.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Collaboration / replication hooks for the canvas graph. -//! -//! A [`SyncPlugin`] sits **beside** the local [`crate::Graph`]: the canvas still applies -//! [`crate::GraphOp`] through commands and history, while the plugin mirrors those intents into a -//! shared model (CRDT, document store, network sync, etc.) and pushes updates back through -//! [`GraphChange`] so the UI stays consistent with peers and with undo/redo semantics. -//! -//! Implementations typically: -//! - In [`SyncPlugin::setup`], subscribe to the shared model and forward diffs on -//! [`UnboundedSender`], setting [`crate::ChangeSource`] (`Local`, `Remote`, `Undo`, -//! …) so the host can tell operator-driven edits from replay or remote merges. -//! - In [`SyncPlugin::process_intent`], apply each [`GraphOp`] produced locally (after a command -//! runs or history replays) into that model, using whatever metadata your stack needs to avoid -//! mis-classifying those writes when your subscription fires again. -//! - In [`SyncPlugin::undo`] / [`SyncPlugin::redo`], advance **your** backend undo manager if the -//! sync layer owns a stack separate from the canvas history. -//! -//! The concrete backend (Yjs, operational transform, file append, etc.) is up to the plugin; this -//! trait only defines the integration surface with the canvas. - -use futures::channel::mpsc::UnboundedSender; -use gpui::{AnyElement, Pixels, Point}; - -use crate::{FlowEvent, GraphChange, GraphOp, RenderContext, Viewport}; - -/// Bridges local graph edits to a replicated or external graph model, and streams model changes -/// back into the canvas. -/// -/// **Data flow (intended pattern)** -/// 1. User action → canvas runs a command → [`GraphOp`]s are applied to the local graph. -/// 2. The host forwards those ops to [`SyncPlugin::process_intent`] so the plugin updates its -/// shared state. -/// 3. Shared state emits updates (local echo, remote peer, or undo replay) → plugin sends -/// [`GraphChange`] on the channel passed to [`SyncPlugin::setup`]. -/// 4. Canvas applies those changes and refreshes; [`GraphChange::source`] distinguishes how each -/// change should be treated (e.g. skip re-broadcasting remote edits). -/// -/// Keep [`process_intent`](SyncPlugin::process_intent) idempotent with respect to your own -/// observers where possible: the same logical op may be reflected back through your subscription; -/// tagging “local intent” vs “remote” vs “undo” origins is the usual way to stay consistent. -pub trait SyncPlugin { - fn name(&self) -> &'static str; - - /// One-time wiring: subscribe to the shared model, retain subscriptions for the plugin - /// lifetime, and send [`GraphChange`] values on `change_sender` whenever the model moves. - /// - /// The host owns the receiver; do not block the UI thread on long-running I/O—spawn a task or - /// use non-blocking channels as appropriate. - fn setup(&mut self, change_sender: UnboundedSender); - - /// Apply a single local [`GraphOp`] (or a batch already decomposed by the host) into your - /// backend. This is invoked for operator-driven edits after they hit the local graph, not as - /// a replacement for the canvas command pipeline. - fn process_intent(&self, op: GraphOp); - - /// Step the sync-layer undo stack backward, if your backend maintains one in addition to (or - /// instead of) mirroring canvas history. - fn undo(&mut self); - /// Step the sync-layer undo stack forward. - fn redo(&mut self); - - /// Optional: handle canvas [`FlowEvent`]s for awareness, presence, or other non-[`GraphOp`] - /// signals. Use [`SyncPluginContext`] for coordinate transforms when needed. - fn on_event(&mut self, _event: &FlowEvent, _ctx: &mut SyncPluginContext); - - /// Optional overlay (e.g. remote pointers) drawn with normal canvas [`RenderContext`]. - fn render(&mut self, _ctx: &mut RenderContext) -> Vec { - vec![] - } -} - -pub struct SyncPluginContext<'a> { - viewport: &'a Viewport, -} - -impl<'a> SyncPluginContext<'a> { - pub(crate) fn new(viewport: &'a Viewport) -> Self { - Self { viewport } - } - - pub fn screen_to_world(&self, screen: Point) -> Point { - self.viewport.screen_to_world(screen) - } - - pub fn world_to_screen(&self, world: Point) -> Point { - self.viewport.world_to_screen(world) - } -} diff --git a/crates/ferrum-flow/src/plugin/utils.rs b/crates/ferrum-flow/src/plugin/utils.rs deleted file mode 100644 index 73116173b1..0000000000 --- a/crates/ferrum-flow/src/plugin/utils.rs +++ /dev/null @@ -1,85 +0,0 @@ -use gpui::KeyDownEvent; - -use crate::{Edge, Graph, GraphChangeKind, NodeId, Viewport, canvas::PortLayoutCache}; - -/// Clears [`PortLayoutCache`] entries affected by an incoming graph change. Call **before** -/// [`Graph::apply`](crate::graph::Graph::apply) so `PortRemoved` can still resolve `node_id`. -pub fn invalidate_port_layout_cache_for_graph_change( - cache: &mut PortLayoutCache, - graph: &Graph, - kind: &GraphChangeKind, -) { - match kind { - GraphChangeKind::NodeRemoved { id } => cache.clear_node(id), - GraphChangeKind::NodeAdded(node) => cache.clear_node(&node.id()), - GraphChangeKind::NodeSetWidthed { id, .. } - | GraphChangeKind::NodeSetHeighted { id, .. } - | GraphChangeKind::NodeDataUpdated { id, .. } => cache.clear_node(id), - GraphChangeKind::PortAdded(port) => cache.clear_node(&port.node_id()), - GraphChangeKind::PortRemoved { id } => { - if let Some(p) = graph.get_port(id) { - cache.clear_node(&p.node_id()); - } - } - GraphChangeKind::NodeMoved { .. } - | GraphChangeKind::NodeOrderUpdate(_) - | GraphChangeKind::EdgeAdded(_) - | GraphChangeKind::EdgeRemoved { .. } - | GraphChangeKind::RedrawRequested => {} - GraphChangeKind::Batch(changes) => { - for c in changes { - invalidate_port_layout_cache_for_graph_change(cache, graph, c); - } - } - } -} - -/// Primary shortcut modifier: ⌘ on macOS, Ctrl on other platforms. -pub fn primary_platform_modifier(ev: &KeyDownEvent) -> bool { - #[cfg(target_os = "macos")] - { - ev.keystroke.modifiers.platform - } - #[cfg(not(target_os = "macos"))] - { - ev.keystroke.modifiers.control - } -} - -pub fn is_node_visible(graph: &Graph, viewport: &Viewport, node_id: &NodeId) -> bool { - let Some(node) = graph.get_node(node_id) else { - return false; - }; - - viewport.is_node_visible(node) -} - -pub fn is_edge_visible(graph: &Graph, viewport: &Viewport, edge: &Edge) -> bool { - let Edge { - source_port, - target_port, - .. - } = edge; - - let Some(port) = graph.get_port(source_port) else { - return false; - }; - let n1 = port.node_id(); - - let Some(port) = graph.get_port(target_port) else { - return false; - }; - let n2 = port.node_id(); - - let node1_visible = graph - .get_node(&n1) - .map(|n| viewport.is_node_visible(n)) - .unwrap_or(false); - - let node2_visible = graph - .get_node(&n2) - .map(|n| viewport.is_node_visible(n)) - .unwrap_or(false); - - node1_visible || node2_visible -} diff --git a/crates/ferrum-flow/src/plugin_testing.rs b/crates/ferrum-flow/src/plugin_testing.rs deleted file mode 100644 index ae5161217f..0000000000 --- a/crates/ferrum-flow/src/plugin_testing.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Helpers for testing [`Plugin`](crate::Plugin) implementations. -//! -//! Enable the **`testing`** Cargo feature on `ferrum-flow` to use this module: -//! -//! ```toml -//! ferrum-flow = { version = "…", features = ["testing"] } -//! ``` -//! -//! This harness is intended for plugin unit/integration tests in downstream crates where -//! [`InitPluginContext`], [`PluginContext`] and [`RenderContext`] constructors are intentionally -//! not public. - -use gpui::{AnyElement, Context, Pixels, Size, Window, px}; -use std::time::Duration; - -use crate::{ - EventResult, FlowCanvas, FlowEvent, FlowTheme, Graph, LocalHistory, Plugin, PluginContext, - RenderContext, RendererRegistry, SharedState, SyncPlugin, Viewport, - canvas::{InteractionState, PortLayoutCache}, - plugin::InitPluginContext, -}; - -/// Test harness that can drive plugin `setup`, `on_event`, and `render` with realistic internal -/// contexts. -pub struct PluginTestHarness { - pub graph: Graph, - pub port_offset_cache: PortLayoutCache, - pub viewport: Viewport, - pub interaction: InteractionState, - pub renderers: RendererRegistry, - pub history: LocalHistory, - pub theme: FlowTheme, - pub shared_state: SharedState, - sync_plugin: Option>, - emitted_events: Vec, - notify_count: usize, -} - -impl PluginTestHarness { - pub fn new(graph: Graph) -> Self { - Self { - graph, - port_offset_cache: PortLayoutCache::new(), - viewport: Viewport::new(), - interaction: InteractionState::new(), - renderers: RendererRegistry::new(), - history: LocalHistory::new(), - theme: FlowTheme::default(), - shared_state: SharedState::new(), - sync_plugin: None, - emitted_events: Vec::new(), - notify_count: 0, - } - } - - /// Runs `Plugin::setup`. - /// - /// Call this only in tests that already have a GPUI context/window. - pub fn run_setup<'a, 'b>( - &mut self, - plugin: &mut dyn Plugin, - gpui_ctx: &'a Context<'b, FlowCanvas>, - drawable_size: Size, - ) { - let mut ctx = InitPluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.renderers, - gpui_ctx, - drawable_size, - &mut self.theme, - &mut self.shared_state, - ); - plugin.setup(&mut ctx); - } - - /// Runs `Plugin::on_event` once and captures emitted events / notify calls. - pub fn run_event(&mut self, plugin: &mut dyn Plugin, event: FlowEvent) -> EventResult { - let emitted_events = &mut self.emitted_events; - let notify_count = &mut self.notify_count; - let mut emit = |e: FlowEvent| { - emitted_events.push(e); - }; - let mut notify = || { - *notify_count += 1; - }; - let mut schedule_after = |_delay: Duration| {}; - let mut ctx = PluginContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &mut self.viewport, - &mut self.interaction, - &mut self.renderers, - &mut self.sync_plugin, - &mut self.history, - &mut self.theme, - &mut self.shared_state, - &mut emit, - &mut notify, - &mut schedule_after, - ); - plugin.on_event(&event, &mut ctx) - } - - /// Runs `Plugin::render` once. - /// - /// Call this only in tests that already have a GPUI window. - pub fn run_render(&mut self, plugin: &mut dyn Plugin, window: &Window) -> Option { - let mut ctx = RenderContext::new( - &mut self.graph, - &mut self.port_offset_cache, - &self.viewport, - &self.renderers, - window, - &self.theme, - &self.shared_state, - ); - plugin.render(&mut ctx) - } - - /// Returns number of times `ctx.notify()` was called during `run_event`. - pub fn notify_count(&self) -> usize { - self.notify_count - } - - /// Drains and returns custom/input events emitted via `ctx.emit(...)`. - pub fn drain_emitted_events(&mut self) -> Vec { - std::mem::take(&mut self.emitted_events) - } -} - -impl Default for PluginTestHarness { - fn default() -> Self { - let mut harness = Self::new(Graph::new()); - harness.viewport.set_window_bounds(Some(gpui::Bounds::new( - gpui::Point::new(px(0.0), px(0.0)), - gpui::Size::new(px(800.0), px(600.0)), - ))); - harness - } -} diff --git a/crates/ferrum-flow/src/plugins/align.rs b/crates/ferrum-flow/src/plugins/align.rs deleted file mode 100644 index 699ce0c38f..0000000000 --- a/crates/ferrum-flow/src/plugins/align.rs +++ /dev/null @@ -1,166 +0,0 @@ -use gpui::{Pixels, Point, px}; - -use crate::{ - NodeId, - plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier}, - plugins::node::DragNodesCommand, -}; - -/// Align selected nodes to their shared bounding box (⌘⇧L/R/T/B/H/V or Ctrl⇧…). -pub struct AlignPlugin; - -#[derive(Clone, Copy)] -enum AlignKind { - Left, - Right, - Top, - Bottom, - CenterH, - CenterV, -} - -type NodePositions = Vec<(NodeId, Point)>; -type AlignFromTo = (NodePositions, NodePositions); - -impl AlignPlugin { - pub fn new() -> Self { - Self - } -} - -impl Default for AlignPlugin { - fn default() -> Self { - Self::new() - } -} - -fn align_shortcut(ev: &gpui::KeyDownEvent) -> bool { - primary_platform_modifier(ev) && ev.keystroke.modifiers.shift -} - -fn px_to_f32(p: Pixels) -> f32 { - p.into() -} - -fn f32_neq(a: f32, b: f32) -> bool { - (a - b).abs() > 0.01 -} - -fn selected_nodes_ordered(ctx: &PluginContext) -> Vec { - ctx.graph - .node_order() - .iter() - .filter(|id| ctx.graph.selected_node().contains(id)) - .copied() - .collect() -} - -fn build_aligned_positions(ctx: &PluginContext, kind: AlignKind) -> Option { - let ids = selected_nodes_ordered(ctx); - if ids.len() < 2 { - return None; - } - - let mut min_left = f32::INFINITY; - let mut max_right = f32::NEG_INFINITY; - let mut min_top = f32::INFINITY; - let mut max_bottom = f32::NEG_INFINITY; - - for id in &ids { - let n = ctx.get_node(id)?; - let (nx, ny) = n.position(); - let size = *n.size_ref(); - let x = px_to_f32(nx); - let y = px_to_f32(ny); - let w = px_to_f32(size.width); - let h = px_to_f32(size.height); - min_left = min_left.min(x); - max_right = max_right.max(x + w); - min_top = min_top.min(y); - max_bottom = max_bottom.max(y + h); - } - - let center_x = (min_left + max_right) / 2.0; - let center_y = (min_top + max_bottom) / 2.0; - - let mut from = Vec::with_capacity(ids.len()); - let mut to = Vec::with_capacity(ids.len()); - - for id in ids { - let n = ctx.get_node(&id)?; - let p = n.point(); - from.push((id, p)); - - let (nx, ny) = n.position(); - let size = *n.size_ref(); - let x = px_to_f32(nx); - let y = px_to_f32(ny); - let w = px_to_f32(size.width); - let h = px_to_f32(size.height); - - let (nx, ny) = match kind { - AlignKind::Left => (min_left, y), - AlignKind::Right => (max_right - w, y), - AlignKind::Top => (x, min_top), - AlignKind::Bottom => (x, max_bottom - h), - AlignKind::CenterH => (center_x - w / 2.0, y), - AlignKind::CenterV => (x, center_y - h / 2.0), - }; - to.push((id, Point::new(px(nx), px(ny)))); - } - - let changed = from.iter().zip(to.iter()).any(|((_, pf), (_, pt))| { - f32_neq(px_to_f32(pf.x), px_to_f32(pt.x)) || f32_neq(px_to_f32(pf.y), px_to_f32(pt.y)) - }); - if !changed { - return None; - } - - Some((from, to)) -} - -fn apply_align(ctx: &mut PluginContext, kind: AlignKind) { - let Some((from, to)) = build_aligned_positions(ctx, kind) else { - return; - }; - ctx.execute_command(DragNodesCommand::from_positions(from, to)); - ctx.cache_all_node_port_offset(); -} - -impl Plugin for AlignPlugin { - fn name(&self) -> &'static str { - "align" - } - - fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {} - - fn priority(&self) -> i32 { - 91 - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event { - if !align_shortcut(ev) { - return crate::plugin::EventResult::Continue; - } - let kind = match ev.keystroke.key.as_str() { - "l" => Some(AlignKind::Left), - "r" => Some(AlignKind::Right), - "t" => Some(AlignKind::Top), - "b" => Some(AlignKind::Bottom), - "h" => Some(AlignKind::CenterH), - "v" => Some(AlignKind::CenterV), - _ => None, - }; - if let Some(kind) = kind { - apply_align(ctx, kind); - return crate::plugin::EventResult::Stop; - } - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/background.rs b/crates/ferrum-flow/src/plugins/background.rs deleted file mode 100644 index 8ae2d002d3..0000000000 --- a/crates/ferrum-flow/src/plugins/background.rs +++ /dev/null @@ -1,206 +0,0 @@ -use crate::plugin::Plugin; -use gpui::{ - Bounds, Corners, Element as _, InteractiveElement as _, ParentElement, RenderImage, Size, - Styled, canvas, div, px, -}; -use image::{Frame, RgbaImage}; -use smallvec::smallvec; -use std::sync::Arc; - -const BASE_GRID: f32 = 40.0; - -#[derive(Clone, Copy, PartialEq)] -struct BitmapKey { - offset_x_mod: i32, // (offset_x % grid * 1000) as i32 - offset_y_mod: i32, - grid_i: i32, // (grid * 1000) as i32 - width: u32, - height: u32, - bg_color: u32, - dot_color: u32, -} - -fn generate_fullscreen_bitmap( - width: u32, - height: u32, - grid: f32, - start_x: f32, - start_y: f32, - bg_color: u32, - dot_color: u32, -) -> Arc { - let w = width as usize; - let h = height as usize; - - let bg = [ - ((bg_color >> 16) & 0xFF) as u8, - ((bg_color >> 8) & 0xFF) as u8, - (bg_color & 0xFF) as u8, - 255u8, - ]; - let dot = [ - ((dot_color >> 16) & 0xFF) as u8, - ((dot_color >> 8) & 0xFF) as u8, - (dot_color & 0xFF) as u8, - 255u8, - ]; - - let mut data = vec![0u8; w * h * 4]; - for i in 0..w * h { - let p = i * 4; - data[p] = bg[0]; - data[p + 1] = bg[1]; - data[p + 2] = bg[2]; - data[p + 3] = 255; - } - - let mut x = start_x; - while x < width as f32 { - let mut y = start_y; - while y < height as f32 { - for dy in 0..2i32 { - for dx in 0..2i32 { - let px = (x - 1.0 + dx as f32).floor() as isize; - let py = (y - 1.0 + dy as f32).floor() as isize; - if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h { - let i = ((py as usize) * w + (px as usize)) * 4; - data[i] = dot[0]; - data[i + 1] = dot[1]; - data[i + 2] = dot[2]; - data[i + 3] = 255; - } - } - } - y += grid; - } - x += grid; - } - - for chunk in data.chunks_exact_mut(4) { - chunk.swap(0, 2); - } - - let img = RgbaImage::from_raw(width, height, data).unwrap(); - Arc::new(RenderImage::new(smallvec![Frame::new(img)])) -} - -pub struct BackgroundPlugin { - bitmap_key: Option, - bitmap: Option>, -} - -impl Default for BackgroundPlugin { - fn default() -> Self { - Self::new() - } -} - -impl BackgroundPlugin { - pub fn new() -> Self { - Self { - bitmap_key: None, - bitmap: None, - } - } - - fn sync_bitmap(&mut self, ctx: &crate::plugin::RenderContext) { - let zoom = ctx.zoom(); - let grid = BASE_GRID * zoom; - let offset = ctx.offset(); - let offset_x = f32::from(offset.x); - let offset_y = f32::from(offset.y); - let bounds = ctx.window.bounds(); - let width = f32::from(bounds.size.width) as u32; - let height = f32::from(bounds.size.height) as u32; - - if grid <= 0.0 || width == 0 || height == 0 { - return; - } - - let ox_mod = offset_x % grid; - let oy_mod = offset_y % grid; - - let key = BitmapKey { - offset_x_mod: (ox_mod * 1000.0) as i32, - offset_y_mod: (oy_mod * 1000.0) as i32, - grid_i: (grid * 1000.0) as i32, - width, - height, - bg_color: ctx.theme.background, - dot_color: ctx.theme.background_grid_dot, - }; - - if self.bitmap_key == Some(key) { - return; - } - - self.bitmap_key = Some(key); - self.bitmap = Some(generate_fullscreen_bitmap( - width, - height, - grid, - ox_mod, - oy_mod, - ctx.theme.background, - ctx.theme.background_grid_dot, - )); - } -} - -impl Plugin for BackgroundPlugin { - fn name(&self) -> &'static str { - "background" - } - fn priority(&self) -> i32 { - 0 - } - fn render_layer(&self) -> crate::plugin::RenderLayer { - crate::plugin::RenderLayer::Background - } - - fn render(&mut self, ctx: &mut crate::plugin::RenderContext) -> Option { - self.sync_bitmap(ctx); - - let Some(bitmap) = self.bitmap.as_ref().map(Arc::clone) else { - return Some( - div() - .id("background") - .absolute() - .size_full() - .bg(gpui::rgb(ctx.theme.background)) - .into_any(), - ); - }; - - let bounds = ctx.window.bounds(); - let width = f32::from(bounds.size.width); - let height = f32::from(bounds.size.height); - - let el = canvas( - move |_, _, _| bitmap, - move |bounds, bitmap, window, _cx| { - let _ = window.paint_image( - Bounds { - origin: bounds.origin, - size: Size::new(px(width), px(height)), - }, - Corners::default(), - Arc::clone(&bitmap), - 0, - false, - ); - }, - ) - .absolute() - .size_full(); - - Some( - div() - .id("background") - .absolute() - .size_full() - .child(el) - .into_any(), - ) - } -} diff --git a/crates/ferrum-flow/src/plugins/clipboard/clipboard_ops.rs b/crates/ferrum-flow/src/plugins/clipboard/clipboard_ops.rs deleted file mode 100644 index 6437aa857e..0000000000 --- a/crates/ferrum-flow/src/plugins/clipboard/clipboard_ops.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use gpui::{Pixels, Point, px}; - -use crate::{CompositeCommand, Edge, Graph, Node, Port, plugin::PluginContext}; - -use super::copied_subgraph::CopiedSubgraph; -use crate::plugins::{CreateEdge, CreateNode, CreatePort}; - -#[derive(Clone)] -pub(crate) struct ClipboardShared(pub CopiedSubgraph); - -pub(crate) fn set_clipboard_subgraph(ctx: &mut PluginContext, sub: CopiedSubgraph) { - ctx.shared_state.insert(ClipboardShared(sub)); -} - -pub(crate) fn get_clipboard_subgraph(ctx: &PluginContext) -> Option { - ctx.shared_state - .get::() - .map(|s| s.0.clone()) -} - -pub(crate) fn has_clipboard_subgraph(ctx: &PluginContext) -> bool { - ctx.shared_state.contains::() -} - -pub(crate) fn extract_subgraph(graph: &Graph) -> Option { - if graph.selected_node_is_empty() { - return None; - } - let node_ids = graph.selected_node(); - if node_ids.is_empty() { - return None; - } - - let mut port_ids = HashSet::new(); - let mut nodes = Vec::with_capacity(node_ids.len()); - let mut ports = Vec::new(); - for nid in node_ids { - let n = graph.get_node(nid)?; - for pid in n.inputs().iter().chain(n.outputs().iter()) { - port_ids.insert(*pid); - if let Some(p) = graph.get_port(pid) { - ports.push(p.clone()); - } - } - nodes.push(n.clone()); - } - - let edges = graph - .edges_values() - .filter(|e| port_ids.contains(&e.source_port) && port_ids.contains(&e.target_port)) - .cloned() - .collect(); - - Some(CopiedSubgraph { - nodes, - ports, - edges, - }) -} - -/// Top-left of the axis-aligned bounding box of copied node positions (world space). -fn subgraph_bounds_top_left(sub: &CopiedSubgraph) -> Point { - let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY); - for n in &sub.nodes { - let (x, y) = n.position(); - min_x = min_x.min(x.into()); - min_y = min_y.min(y.into()); - } - Point::new(px(min_x), px(min_y)) -} - -/// Paste with the subgraph's bounding-box top-left placed at `anchor_world`. -pub(crate) fn paste_subgraph_at_world( - ctx: &mut PluginContext, - sub: &CopiedSubgraph, - anchor_world: Point, -) { - paste_subgraph_with_anchor(ctx, sub, anchor_world); -} - -/// Paste offset from the copied layout (keyboard paste): bbox top-left moves by (40, 40) in world space. -pub(crate) fn paste_subgraph(ctx: &mut PluginContext, sub: &CopiedSubgraph) { - const NUDGE: f32 = 40.0; - let origin = subgraph_bounds_top_left(sub); - let anchor = Point::new(origin.x + px(NUDGE), origin.y + px(NUDGE)); - paste_subgraph_with_anchor(ctx, sub, anchor); -} - -fn paste_subgraph_with_anchor( - ctx: &mut PluginContext, - sub: &CopiedSubgraph, - anchor_world: Point, -) { - if sub.nodes.is_empty() { - return; - } - - let origin = subgraph_bounds_top_left(sub); - let ox: f32 = origin.x.into(); - let oy: f32 = origin.y.into(); - let ax: f32 = anchor_world.x.into(); - let ay: f32 = anchor_world.y.into(); - - let mut node_map = HashMap::new(); - for n in &sub.nodes { - node_map.insert(n.id(), ctx.graph.next_node_id()); - } - let mut port_map = HashMap::new(); - for p in &sub.ports { - port_map.insert(p.id(), ctx.graph.next_port_id()); - } - - let mut composite = CompositeCommand::new(); - - let mut new_node_ids = Vec::new(); - - for old in &sub.nodes { - let new_id = node_map[&old.id()]; - let (x, y) = old.position(); - let nx = ax + f32::from(x) - ox; - let ny = ay + f32::from(y) - oy; - let mut node = Node::new(nx, ny); - node.set_renderer_key(old.renderer_key()); - node.set_execute_type(old.execute_type_ref()); - node.set_size_mut(*old.size_ref()); - node.set_data(old.data_ref().clone()); - node.set_id(new_id); - - for pid in old.inputs() { - node.push_input(port_map[pid]); - } - for pid in old.outputs() { - node.push_output(port_map[pid]); - } - new_node_ids.push(new_id); - composite.push(CreateNode::new(node)); - } - - for old in &sub.ports { - let port = Port::new( - port_map[&old.id()], - old.kind(), - old.index(), - node_map[&old.node_id()], - old.position(), - *old.size_ref(), - old.port_type_ref().clone(), - ); - composite.push(CreatePort::new(port)); - } - - for old in &sub.edges { - let edge = Edge { - id: ctx.graph.next_edge_id(), - source_port: port_map[&old.source_port], - target_port: port_map[&old.target_port], - }; - composite.push(CreateEdge::new(edge)); - } - - let pasted_ids = sub.nodes.iter().map(|n| node_map[&n.id()]); - - ctx.execute_command(composite); - ctx.clear_selected_edge(); - ctx.clear_selected_node(); - for nid in pasted_ids { - ctx.add_selected_node(nid, true); - } - ctx.cache_port_offset_with_node(&new_node_ids); -} diff --git a/crates/ferrum-flow/src/plugins/clipboard/copied_subgraph.rs b/crates/ferrum-flow/src/plugins/clipboard/copied_subgraph.rs deleted file mode 100644 index f0deae69a2..0000000000 --- a/crates/ferrum-flow/src/plugins/clipboard/copied_subgraph.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::{Edge, Node, Port}; - -#[derive(Clone)] -pub struct CopiedSubgraph { - pub(crate) nodes: Vec, - pub(crate) ports: Vec, - pub(crate) edges: Vec, -} diff --git a/crates/ferrum-flow/src/plugins/clipboard/mod.rs b/crates/ferrum-flow/src/plugins/clipboard/mod.rs deleted file mode 100644 index a0e9c75c2e..0000000000 --- a/crates/ferrum-flow/src/plugins/clipboard/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod clipboard_ops; -mod copied_subgraph; -mod plugin; - -pub use plugin::ClipboardPlugin; - -pub(crate) use clipboard_ops::{ - extract_subgraph, get_clipboard_subgraph, has_clipboard_subgraph, paste_subgraph_at_world, - set_clipboard_subgraph, -}; diff --git a/crates/ferrum-flow/src/plugins/clipboard/plugin.rs b/crates/ferrum-flow/src/plugins/clipboard/plugin.rs deleted file mode 100644 index 8deb1eb5f9..0000000000 --- a/crates/ferrum-flow/src/plugins/clipboard/plugin.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier}; - -use super::clipboard_ops::{ - extract_subgraph, get_clipboard_subgraph, paste_subgraph, set_clipboard_subgraph, -}; - -/// Copy / paste selected nodes, their ports, and edges **between** those ports (one undo on paste). -pub struct ClipboardPlugin; - -impl ClipboardPlugin { - pub fn new() -> Self { - Self - } -} - -impl Default for ClipboardPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for ClipboardPlugin { - fn name(&self) -> &'static str { - "clipboard" - } - - fn priority(&self) -> i32 { - 95 - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event { - if !primary_platform_modifier(ev) { - return crate::plugin::EventResult::Continue; - } - match ev.keystroke.key.as_str() { - "c" => { - if let Some(sub) = extract_subgraph(ctx.graph) { - set_clipboard_subgraph(ctx, sub); - } - return crate::plugin::EventResult::Stop; - } - "v" => { - if let Some(sub) = get_clipboard_subgraph(ctx) { - paste_subgraph(ctx, &sub); - } - return crate::plugin::EventResult::Stop; - } - _ => {} - } - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/context_menu.rs b/crates/ferrum-flow/src/plugins/context_menu.rs deleted file mode 100644 index bc656632f8..0000000000 --- a/crates/ferrum-flow/src/plugins/context_menu.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::sync::Arc; - -use gpui::{ - IntoElement as _, MouseButton, ParentElement as _, Pixels, Point, SharedString, Styled as _, - div, px, rgb, -}; - -use crate::{ - NodeId, - plugin::{ - EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer, - }, -}; - -use super::{ - clipboard::{ - extract_subgraph, get_clipboard_subgraph, has_clipboard_subgraph, paste_subgraph_at_world, - set_clipboard_subgraph, - }, - delete::delete_selection, - fit_all::fit_entire_graph, - focus_selection::focus_viewport_on_selection, - select_all_viewport::select_all_in_viewport, -}; - -const MENU_W: f32 = 228.0; -const ROW_H: f32 = 26.0; -const SEP_H: f32 = 9.0; -const MENU_PAD: f32 = 4.0; - -/// Callback invoked when the user picks a custom canvas menu row (e.g. open an input dialog in the app). -/// -/// The second argument is the **world-space** point under the initial right-click that opened this menu -/// (same as [`PluginContext::screen_to_world`] applied to that click). -type ContextMenuActionFn = dyn for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync; - -#[derive(Clone)] -pub struct ContextMenuCustomAction(Arc); - -impl ContextMenuCustomAction { - pub fn new( - f: impl for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync + 'static, - ) -> Self { - Self(Arc::new(f)) - } - - fn call(&self, ctx: &mut PluginContext<'_>, menu_world: Point) { - (self.0)(ctx, menu_world); - } -} - -/// One extra row on the **canvas background** context menu (after built-in items). -#[derive(Clone)] -pub struct ContextMenuCanvasExtra { - pub label: SharedString, - pub shortcut: Option, - pub on_select: ContextMenuCustomAction, -} - -impl ContextMenuCanvasExtra { - pub fn new( - label: impl Into, - on_select: impl for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync + 'static, - ) -> Self { - Self { - label: label.into(), - shortcut: None, - on_select: ContextMenuCustomAction::new(on_select), - } - } - - pub fn with_shortcut( - label: impl Into, - shortcut: impl Into, - on_select: impl for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync + 'static, - ) -> Self { - Self { - label: label.into(), - shortcut: Some(shortcut.into()), - on_select: ContextMenuCustomAction::new(on_select), - } - } -} - -/// Right-click menu on the canvas (empty area) or on a node. Optional [`ContextMenuCanvasExtra`] rows -/// are appended after built-in canvas actions. -pub struct ContextMenuPlugin { - open: Option, - canvas_extras: Vec, -} - -#[derive(Clone, Copy)] -enum MenuBuiltin { - FitAllGraph, - Paste, - SelectAllViewport, - FocusSelection, - Copy, - Delete, - BringToFront(NodeId), -} - -#[derive(Clone)] -enum MenuItem { - Separator, - Builtin(MenuBuiltin), - Custom { - label: SharedString, - shortcut: Option, - action: ContextMenuCustomAction, - }, -} - -#[derive(Clone)] -struct OpenMenu { - anchor: Point, - /// World position of the right-click that opened this menu. - anchor_world: Point, - actions: Vec, -} - -impl Default for ContextMenuPlugin { - fn default() -> Self { - Self::new() - } -} - -impl ContextMenuPlugin { - pub fn new() -> Self { - Self { - open: None, - canvas_extras: Vec::new(), - } - } - - pub fn with_canvas_extras(canvas_extras: Vec) -> Self { - Self { - open: None, - canvas_extras, - } - } - - /// Append a canvas-background row with a custom label (e.g. “Add node…” → show input in meili). - pub fn canvas_row( - mut self, - label: impl Into, - on_select: impl for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync + 'static, - ) -> Self { - self.canvas_extras - .push(ContextMenuCanvasExtra::new(label, on_select)); - self - } - - /// Same as [`Self::canvas_row`] but with a shortcut hint string shown on the right. - pub fn canvas_row_with_shortcut( - mut self, - label: impl Into, - shortcut: impl Into, - on_select: impl for<'a> Fn(&mut PluginContext<'a>, Point) + Send + Sync + 'static, - ) -> Self { - self.canvas_extras - .push(ContextMenuCanvasExtra::with_shortcut( - label, shortcut, on_select, - )); - self - } - - fn row_height(action: &MenuItem) -> f32 { - match action { - MenuItem::Separator => SEP_H, - _ => ROW_H, - } - } - - fn content_height(actions: &[MenuItem]) -> f32 { - actions.iter().map(Self::row_height).sum() - } - - fn menu_bounds(anchor: Point, actions: &[MenuItem]) -> gpui::Bounds { - let h = Self::content_height(actions) + MENU_PAD * 2.0; - gpui::Bounds::new(anchor, gpui::Size::new(px(MENU_W), px(h))) - } - - fn label_builtin(b: MenuBuiltin) -> &'static str { - match b { - MenuBuiltin::FitAllGraph => "Fit entire graph", - MenuBuiltin::Paste => "Paste", - MenuBuiltin::SelectAllViewport => "Select all in view", - MenuBuiltin::FocusSelection => "Focus selection", - MenuBuiltin::Copy => "Copy", - MenuBuiltin::Delete => "Delete", - MenuBuiltin::BringToFront(_) => "Bring to front", - } - } - - fn shortcut_hint_builtin(b: MenuBuiltin) -> Option<&'static str> { - #[cfg(target_os = "macos")] - { - match b { - MenuBuiltin::FitAllGraph => Some("⌘0"), - MenuBuiltin::Paste => Some("⌘V"), - MenuBuiltin::SelectAllViewport => Some("⌘A"), - MenuBuiltin::FocusSelection => Some("⌘⇧F"), - MenuBuiltin::Copy => Some("⌘C"), - MenuBuiltin::Delete => Some("⌫"), - MenuBuiltin::BringToFront(_) => None, - } - } - #[cfg(not(target_os = "macos"))] - { - match b { - MenuBuiltin::FitAllGraph => Some("Ctrl+0"), - MenuBuiltin::Paste => Some("Ctrl+V"), - MenuBuiltin::SelectAllViewport => Some("Ctrl+A"), - MenuBuiltin::FocusSelection => Some("Ctrl+Shift+F"), - MenuBuiltin::Copy => Some("Ctrl+C"), - MenuBuiltin::Delete => Some("Del"), - MenuBuiltin::BringToFront(_) => None, - } - } - } - - fn canvas_actions(&self, ctx: &PluginContext) -> Vec { - let mut v = Vec::new(); - v.push(MenuItem::Builtin(MenuBuiltin::FitAllGraph)); - v.push(MenuItem::Separator); - if has_clipboard_subgraph(ctx) { - v.push(MenuItem::Builtin(MenuBuiltin::Paste)); - v.push(MenuItem::Separator); - } - v.push(MenuItem::Builtin(MenuBuiltin::SelectAllViewport)); - v.push(MenuItem::Separator); - v.push(MenuItem::Builtin(MenuBuiltin::FocusSelection)); - for e in &self.canvas_extras { - v.push(MenuItem::Separator); - v.push(MenuItem::Custom { - label: e.label.clone(), - shortcut: e.shortcut.clone(), - action: e.on_select.clone(), - }); - } - v - } - - fn node_actions(nid: NodeId) -> Vec { - vec![ - MenuItem::Builtin(MenuBuiltin::Copy), - MenuItem::Separator, - MenuItem::Builtin(MenuBuiltin::Delete), - MenuItem::Builtin(MenuBuiltin::BringToFront(nid)), - MenuItem::Separator, - MenuItem::Builtin(MenuBuiltin::FocusSelection), - ] - } - - fn run_action(ctx: &mut PluginContext, action: &MenuItem, menu_world: Point) { - match action { - MenuItem::Separator => {} - MenuItem::Builtin(b) => match b { - MenuBuiltin::FitAllGraph => fit_entire_graph(ctx), - MenuBuiltin::Paste => { - if let Some(sub) = get_clipboard_subgraph(ctx) { - paste_subgraph_at_world(ctx, &sub, menu_world); - } - } - MenuBuiltin::SelectAllViewport => select_all_in_viewport(ctx), - MenuBuiltin::FocusSelection => focus_viewport_on_selection(ctx), - MenuBuiltin::Copy => { - if let Some(s) = extract_subgraph(ctx.graph) { - set_clipboard_subgraph(ctx, s); - } - } - MenuBuiltin::Delete => delete_selection(ctx), - MenuBuiltin::BringToFront(id) => ctx.bring_node_to_front(*id), - }, - MenuItem::Custom { action, .. } => action.call(ctx, menu_world), - } - ctx.notify(); - } - - fn row_at_dy(actions: &[MenuItem], dy: f32) -> Option { - if dy < 0.0 { - return None; - } - let mut y = 0.0; - for (i, a) in actions.iter().enumerate() { - let h = Self::row_height(a); - if dy < y + h { - return Some(i); - } - y += h; - } - None - } -} - -impl Plugin for ContextMenuPlugin { - fn name(&self) -> &'static str { - "context_menu" - } - - fn priority(&self) -> i32 { - 132 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let open = self.open.as_ref()?; - let panel_bg = ctx.theme.context_menu_background; - let panel_border = ctx.theme.context_menu_border; - let row_text = ctx.theme.context_menu_text; - let shortcut_text = ctx.theme.context_menu_shortcut_text; - let separator = ctx.theme.context_menu_separator; - - let rows = open.actions.iter().map(|a| match a { - MenuItem::Separator => div() - .w_full() - .h(px(SEP_H)) - .flex() - .items_center() - .px_2() - .child(div().w_full().h(px(1.0)).bg(rgb(separator))), - MenuItem::Builtin(b) => { - let label = div() - .flex_1() - .min_w(px(0.)) - .overflow_hidden() - .text_ellipsis() - .child(ContextMenuPlugin::label_builtin(*b)); - let shortcut = ContextMenuPlugin::shortcut_hint_builtin(*b).map(|h| { - div() - .flex_shrink_0() - .ml_2() - .text_xs() - .text_color(rgb(shortcut_text)) - .child(h) - }); - div() - .w_full() - .h(px(ROW_H)) - .flex() - .flex_row() - .items_center() - .px_2() - .text_sm() - .text_color(rgb(row_text)) - .child(label) - .children(shortcut) - } - MenuItem::Custom { - label, shortcut, .. - } => { - let label_el = div() - .flex_1() - .min_w(px(0.)) - .overflow_hidden() - .text_ellipsis() - .child(label.clone()); - let shortcut_el = shortcut.as_ref().map(|h| { - div() - .flex_shrink_0() - .ml_2() - .text_xs() - .text_color(rgb(shortcut_text)) - .child(h.clone()) - }); - div() - .w_full() - .h(px(ROW_H)) - .flex() - .flex_row() - .items_center() - .px_2() - .text_sm() - .text_color(rgb(row_text)) - .child(label_el) - .children(shortcut_el) - } - }); - - Some( - div() - .absolute() - .left(open.anchor.x) - .top(open.anchor.y) - .w(px(MENU_W)) - .p_1() - .bg(rgb(panel_bg)) - .border_1() - .border_color(rgb(panel_border)) - .rounded(px(6.0)) - .shadow_sm() - .children(rows) - .into_any_element(), - ) - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event { - if ev.button == MouseButton::Left { - if let Some(open) = self.open.take() { - let menu_world = open.anchor_world; - let b = Self::menu_bounds(open.anchor, &open.actions); - if b.contains(&ev.position) { - let dy: f32 = (ev.position.y - open.anchor.y).into(); - let inner_y = dy - MENU_PAD; - if let Some(row) = Self::row_at_dy(&open.actions, inner_y) { - let a = &open.actions[row]; - if !matches!(a, MenuItem::Separator) { - Self::run_action(ctx, a, menu_world); - } else { - ctx.notify(); - } - } else { - ctx.notify(); - } - return EventResult::Stop; - } - ctx.notify(); - return EventResult::Continue; - } - return EventResult::Continue; - } - - if ev.button == MouseButton::Right { - let world = ctx.screen_to_world(ev.position); - let actions = if let Some(nid) = ctx.hit_node(world) { - if !ctx.graph.selected_node().contains(&nid) { - ctx.clear_selected_edge(); - ctx.clear_selected_node(); - ctx.add_selected_node(nid, false); - } - Self::node_actions(nid) - } else { - self.canvas_actions(ctx) - }; - self.open = Some(OpenMenu { - anchor: ev.position, - anchor_world: world, - actions, - }); - ctx.notify(); - return EventResult::Stop; - } - } - EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/delete.rs b/crates/ferrum-flow/src/plugins/delete.rs deleted file mode 100644 index 1e567d3ec1..0000000000 --- a/crates/ferrum-flow/src/plugins/delete.rs +++ /dev/null @@ -1,278 +0,0 @@ -use crate::{ - Edge, EdgeId, GraphOp, Node, Port, - canvas::Command, - plugin::{FlowEvent, Plugin}, -}; -use std::collections::HashSet; - -pub struct DeletePlugin; - -impl DeletePlugin { - pub fn new() -> Self { - Self {} - } -} - -impl Default for DeletePlugin { - fn default() -> Self { - Self::new() - } -} - -pub(crate) fn delete_selection(ctx: &mut crate::plugin::PluginContext) { - ctx.execute_command(DeleteCommand::new(ctx)); -} - -impl Plugin for DeletePlugin { - fn name(&self) -> &'static str { - "delete" - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event - && (ev.keystroke.key == "delete" || ev.keystroke.key == "backspace") - { - ctx.execute_command(DeleteCommand::new(ctx)); - return crate::plugin::EventResult::Stop; - } - crate::plugin::EventResult::Continue - } -} - -struct DeleteCommand { - selected_edge: Vec, - originally_selected_edge_ids: HashSet, - selected_node: Vec, - selected_port: Vec, -} - -impl DeleteCommand { - fn collect_edges_for_selected_nodes( - graph: &crate::Graph, - selected_nodes: &[Node], - ) -> Vec { - let mut edge_ids = HashSet::new(); - let mut edges = Vec::new(); - - for node in selected_nodes { - for port_id in node.inputs().iter().chain(node.outputs().iter()) { - for edge in graph.edges().values() { - if (edge.source_port == *port_id || edge.target_port == *port_id) - && edge_ids.insert(edge.id) - { - edges.push(edge.clone()); - } - } - } - } - - edges - } - - fn new(ctx: &crate::plugin::PluginContext) -> Self { - let selected_node: Vec = ctx - .graph - .selected_node() - .iter() - .filter_map(|id| ctx.get_node(id).cloned()) - .collect(); - let mut selected_edge: Vec = ctx - .graph - .selected_edge() - .iter() - .filter_map(|id| ctx.graph.get_edge(id).cloned()) - .collect(); - let originally_selected_edge_ids: HashSet<_> = selected_edge.iter().map(|e| e.id).collect(); - let mut seen_edge_ids: HashSet<_> = selected_edge.iter().map(|e| e.id).collect(); - for edge in Self::collect_edges_for_selected_nodes(ctx.graph, &selected_node) { - if seen_edge_ids.insert(edge.id) { - selected_edge.push(edge); - } - } - - Self { - selected_edge, - originally_selected_edge_ids, - selected_port: selected_node - .iter() - .flat_map(|node| node.inputs().iter().chain(node.outputs().iter())) - .filter_map(|port_id| ctx.graph.get_port(port_id).cloned()) - .collect(), - selected_node, - } - } -} - -impl Command for DeleteCommand { - fn name(&self) -> &'static str { - "delete" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.remove_selected_edge(); - ctx.remove_selected_node(); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - for node in &self.selected_node { - ctx.add_node(node.clone()); - ctx.add_selected_node(node.id(), true); - } - - for port in &self.selected_port { - ctx.add_port(port.clone()); - } - - for edge in &self.selected_edge { - ctx.add_edge(edge.clone()); - if self.originally_selected_edge_ids.contains(&edge.id) { - ctx.add_selected_edge(edge.id, true); - } - } - } - - fn to_ops(&self, ctx: &mut crate::CommandContext) -> Vec { - let mut list = vec![]; - let mut removed_edges = HashSet::new(); - for node in &self.selected_node { - list.push(GraphOp::RemoveNode { id: node.id() }); - - let index = ctx.graph.node_order().iter().position(|v| *v == node.id()); - if let Some(index) = index { - list.push(GraphOp::NodeOrderRemove { index }) - } - } - - for port in &self.selected_port { - list.push(GraphOp::RemovePort(port.id())); - } - - for edge in &self.selected_edge { - if removed_edges.insert(edge.id) { - list.push(GraphOp::RemoveEdge(edge.id)); - } - } - - vec![GraphOp::Batch(list)] - } -} - -#[cfg(test)] -mod command_interop_tests { - use std::collections::HashSet; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::DeleteCommand; - - fn delete_command_like_new(graph: &Graph) -> DeleteCommand { - let selected_node: Vec = graph - .selected_node() - .iter() - .filter_map(|id| graph.get_node(id).cloned()) - .collect(); - let mut selected_edge: Vec = graph - .selected_edge() - .iter() - .filter_map(|id| graph.get_edge(id).cloned()) - .collect(); - let originally_selected_edge_ids: HashSet<_> = selected_edge.iter().map(|e| e.id).collect(); - let mut seen_edge_ids: HashSet<_> = selected_edge.iter().map(|e| e.id).collect(); - for edge in DeleteCommand::collect_edges_for_selected_nodes(graph, &selected_node) { - if seen_edge_ids.insert(edge.id) { - selected_edge.push(edge); - } - } - let selected_port: Vec = graph - .selected_node() - .iter() - .filter_map(|node_id| graph.get_node(node_id)) - .flat_map(|node| node.inputs().iter().chain(node.outputs().iter())) - .filter_map(|port_id| graph.get_port(port_id).cloned()) - .collect(); - DeleteCommand { - selected_edge, - originally_selected_edge_ids, - selected_node, - selected_port, - } - } - - #[test] - fn delete_command_interop_single_node_with_port() { - let mut base = Graph::new(); - let src_id = base - .create_node("x") - .position(-220.0, 0.0) - .output() - .build() - .unwrap(); - let dst_id = base - .create_node("x") - .position(220.0, 0.0) - .input() - .output() - .build() - .unwrap(); - let other_id = base - .create_node("x") - .position(440.0, 0.0) - .input() - .build() - .unwrap(); - // Put selected node at the end so execute+undo preserves node_order with current command behavior. - let selected_id = base - .create_node("x") - .position(0.0, 0.0) - .input() - .output() - .build() - .unwrap(); - - let selected_node = base.get_node(&selected_id).expect("selected node").clone(); - let src_node = base.get_node(&src_id).expect("src node").clone(); - let dst_node = base.get_node(&dst_id).expect("dst node").clone(); - let other_node = base.get_node(&other_id).expect("other node").clone(); - - // This edge is NOT selected, but should be deleted via node-cascade. - let _cascade_in = base - .create_edge() - .source(src_node.outputs()[0]) - .target(selected_node.inputs()[0]) - .build() - .expect("cascade in edge"); - // This edge IS selected and also touches selected node. - let selected_edge = base - .create_edge() - .source(selected_node.outputs()[0]) - .target(dst_node.inputs()[0]) - .build() - .expect("selected edge"); - // Unrelated edge should remain untouched. - let _unrelated = base - .create_edge() - .source(dst_node.outputs()[0]) - .target(other_node.inputs()[0]) - .build() - .expect("unrelated edge"); - - base.add_selected_node(selected_id, false); - base.add_selected_edge(selected_edge, true); - - let cmd = delete_command_like_new(&base); - assert_command_interop( - &base, - || { - Box::new(DeleteCommand { - selected_edge: cmd.selected_edge.clone(), - originally_selected_edge_ids: cmd.originally_selected_edge_ids.clone(), - selected_node: cmd.selected_node.clone(), - selected_port: cmd.selected_port.clone(), - }) - }, - "DeleteCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/edge/command.rs b/crates/ferrum-flow/src/plugins/edge/command.rs deleted file mode 100644 index c46d98a084..0000000000 --- a/crates/ferrum-flow/src/plugins/edge/command.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::{collections::HashSet, vec}; - -use crate::{EdgeId, NodeId, canvas::Command, plugin::PluginContext}; - -pub(super) struct SelectEdgeCommand { - edge_id: EdgeId, - shift: bool, - old_selected_edge: HashSet, - old_selected_node: HashSet, -} - -impl SelectEdgeCommand { - pub(super) fn new(edge_id: EdgeId, shift: bool, ctx: &PluginContext) -> Self { - Self { - edge_id, - shift, - old_selected_edge: ctx.graph.selected_edge().clone(), - old_selected_node: ctx.graph.selected_node().clone(), - } - } -} - -impl Command for SelectEdgeCommand { - fn name(&self) -> &'static str { - "select_edge" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - if !self.shift { - ctx.clear_selected_node(); - } - ctx.add_selected_edge(self.edge_id, self.shift); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.graph.set_selected_node(self.old_selected_node.clone()); - ctx.graph.set_selected_edge(self.old_selected_edge.clone()); - } - fn to_ops(&self, ctx: &mut crate::CommandContext) -> Vec { - if !self.shift { - ctx.clear_selected_node(); - } - ctx.add_selected_edge(self.edge_id, self.shift); - vec![] - } -} - -pub(super) struct ClearEdgeCommand { - old_selected_edge: HashSet, -} - -impl ClearEdgeCommand { - pub(super) fn new(ctx: &PluginContext) -> Self { - Self { - old_selected_edge: ctx.graph.selected_edge().clone(), - } - } -} - -impl Command for ClearEdgeCommand { - fn name(&self) -> &'static str { - "clear_edge" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.clear_selected_edge(); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.graph.set_selected_edge(self.old_selected_edge.clone()); - } - - fn to_ops(&self, ctx: &mut crate::CommandContext) -> Vec { - ctx.clear_selected_edge(); - vec![] - } -} - -#[cfg(test)] -mod command_interop_tests { - use crate::{Graph, command_interop::assert_command_interop}; - - use super::{ClearEdgeCommand, SelectEdgeCommand}; - - #[test] - fn select_edge_command_interop() { - let mut base = Graph::new(); - let n1 = base - .create_node("a") - .position(0.0, 0.0) - .output() - .build() - .unwrap(); - let n2 = base - .create_node("b") - .position(100.0, 0.0) - .input() - .build() - .unwrap(); - let n1_node = base.get_node(&n1).expect("n1"); - let n2_node = base.get_node(&n2).expect("n2"); - let source_port = n1_node.outputs()[0]; - let target_port = n2_node.inputs()[0]; - let edge_id = base - .create_edge() - .source(source_port) - .target(target_port) - .build() - .expect("edge"); - - let old_selected_edge = base.selected_edge().clone(); - let old_selected_node = base.selected_node().clone(); - - assert_command_interop( - &base, - || { - Box::new(SelectEdgeCommand { - edge_id, - shift: false, - old_selected_edge: old_selected_edge.clone(), - old_selected_node: old_selected_node.clone(), - }) - }, - "SelectEdgeCommand", - ); - } - - #[test] - fn clear_edge_command_interop() { - let mut base = Graph::new(); - let n1 = base - .create_node("a") - .position(0.0, 0.0) - .output() - .build() - .unwrap(); - let n2 = base - .create_node("b") - .position(100.0, 0.0) - .input() - .build() - .unwrap(); - let n1_node = base.get_node(&n1).expect("n1"); - let n2_node = base.get_node(&n2).expect("n2"); - let source_port = n1_node.outputs()[0]; - let target_port = n2_node.inputs()[0]; - let edge_id = base - .create_edge() - .source(source_port) - .target(target_port) - .build() - .expect("edge"); - base.add_selected_edge(edge_id, false); - - let old_selected_edge = base.selected_edge().clone(); - - assert_command_interop( - &base, - || { - Box::new(ClearEdgeCommand { - old_selected_edge: old_selected_edge.clone(), - }) - }, - "ClearEdgeCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/edge/mod.rs b/crates/ferrum-flow/src/plugins/edge/mod.rs deleted file mode 100644 index 66758c7a7d..0000000000 --- a/crates/ferrum-flow/src/plugins/edge/mod.rs +++ /dev/null @@ -1,286 +0,0 @@ -use std::collections::HashSet; - -use gpui::{ - Bounds, Element, MouseButton, PathBuilder, Pixels, Point, Styled as _, canvas, px, rgb, -}; - -use crate::{ - Edge, EdgeId, RenderContext, - plugin::{FlowEvent, Plugin, PluginContext}, - plugins::edge::command::ClearEdgeCommand, -}; - -mod command; - -use command::SelectEdgeCommand; - -pub struct EdgePlugin {} - -impl EdgePlugin { - pub fn new() -> Self { - Self {} - } -} - -impl Default for EdgePlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for EdgePlugin { - fn name(&self) -> &'static str { - "edge" - } - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::MouseDown(ev)) = event { - if ev.button != MouseButton::Left { - return crate::plugin::EventResult::Continue; - } - let shift = ev.modifiers.shift; - if let Some(id) = hit_test_get_edge(ev.position, ctx) { - ctx.cache_port_offset_with_edge(&id); - ctx.execute_command(SelectEdgeCommand::new(id, shift, ctx)); - return crate::plugin::EventResult::Stop; - } else if !shift { - ctx.execute_command(ClearEdgeCommand::new(ctx)); - } - } - crate::plugin::EventResult::Continue - } - fn priority(&self) -> i32 { - 120 - } - fn render_layer(&self) -> crate::plugin::RenderLayer { - crate::plugin::RenderLayer::Edges - } - fn render(&mut self, ctx: &mut crate::RenderContext) -> Option { - let visible_nodes: HashSet<_> = ctx - .graph - .nodes() - .iter() - .filter(|(_, node)| ctx.is_node_visible_node(node)) - .map(|(id, _)| *id) - .collect(); - - let edges: Vec<_> = ctx - .graph - .edges() - .iter() - .filter(|(_, edge)| { - let Some(source_port) = ctx.graph.get_port(&edge.source_port) else { - return false; - }; - let Some(target_port) = ctx.graph.get_port(&edge.target_port) else { - return false; - }; - - visible_nodes.contains(&source_port.node_id()) - || visible_nodes.contains(&target_port.node_id()) - }) - .map(|(k, v)| (*k, edge_geometry2(v, ctx))) - .collect(); - - let edge_ids = edges.iter().map(|(id, _)| *id); - for edge_id in edge_ids { - ctx.cache_port_offset_with_edge(&edge_id); - } - - let selected_edges = ctx.graph.selected_edge().clone(); - let stroke = ctx.theme.edge_stroke; - let stroke_sel = ctx.theme.edge_stroke_selected; - - Some( - canvas( - move |_, _, _| (edges, selected_edges, stroke, stroke_sel), - move |bounds, (edges, selected_edges, stroke, stroke_sel), win, _| { - let origin = bounds.origin; - for (id, geometry) in edges.iter() { - let Some(EdgeGeometry { start, c1, c2, end }) = geometry else { - return; - }; - let mut line = PathBuilder::stroke(px(1.0)); - line.move_to(*start + origin); - line.cubic_bezier_to(*end + origin, *c1 + origin, *c2 + origin); - - let selected = selected_edges.iter().any(|i| *i == *id); - - if let Ok(line) = line.build() { - win.paint_path(line, rgb(if selected { stroke_sel } else { stroke })); - } - } - }, - ) - .absolute() - .size_full() - .into_any(), - ) - } -} - -pub struct EdgeGeometry { - pub start: Point, - pub c1: Point, - pub c2: Point, - pub end: Point, -} - -fn edge_geometry(edge: &Edge, ctx: &PluginContext) -> Option { - let Edge { - source_port: source_id, - target_port: target_id, - .. - } = edge; - - let start = ctx.port_screen_center_by_port_id(*source_id)?; - let end = ctx.port_screen_center_by_port_id(*target_id)?; - - let source_port = ctx.graph.get_port(source_id)?; - let target_port = ctx.graph.get_port(target_id)?; - - let c1 = ctx.edge_control_point(start, source_port.position()); - let c2 = ctx.edge_control_point(end, target_port.position()); - - Some(EdgeGeometry { start, c1, c2, end }) -} - -fn edge_geometry2(edge: &Edge, ctx: &RenderContext) -> Option { - let Edge { - source_port: source_id, - target_port: target_id, - .. - } = edge; - - let start = ctx.port_screen_center_by_port_id(*source_id)?; - let end = ctx.port_screen_center_by_port_id(*target_id)?; - - let source_port = ctx.graph.get_port(source_id)?; - let target_port = ctx.graph.get_port(target_id)?; - - let c1 = ctx.edge_control_point(start, source_port.position()); - let c2 = ctx.edge_control_point(end, target_port.position()); - - Some(EdgeGeometry { start, c1, c2, end }) -} - -fn hit_test_get_edge(mouse: Point, ctx: &PluginContext) -> Option { - let visible_nodes: HashSet<_> = ctx - .graph - .nodes() - .iter() - .filter(|(_, node)| ctx.is_node_visible_node(node)) - .map(|(id, _)| *id) - .collect(); - - let edges = ctx.graph.edges_values().filter(|edge| { - let Some(source_port) = ctx.graph.get_port(&edge.source_port) else { - return false; - }; - let Some(target_port) = ctx.graph.get_port(&edge.target_port) else { - return false; - }; - - visible_nodes.contains(&source_port.node_id()) - || visible_nodes.contains(&target_port.node_id()) - }); - for edge in edges { - let Some(geom) = edge_geometry(edge, ctx) else { - continue; - }; - - let bound = edge_bounds(&geom); - if !bound.contains(&mouse) { - continue; - } - - if hit_test_edge(mouse, &geom) { - return Some(edge.id); - } - } - - None -} - -pub fn edge_bounds(geom: &EdgeGeometry) -> Bounds { - let min_x = geom.start.x.min(geom.end.x).min(geom.c1.x).min(geom.c2.x); - let max_x = geom.start.x.max(geom.end.x).max(geom.c1.x).max(geom.c2.x); - - let min_y = geom.start.y.min(geom.end.y).min(geom.c1.y).min(geom.c2.y); - let max_y = geom.start.y.max(geom.end.y).max(geom.c1.y).max(geom.c2.y); - - Bounds::from_corners( - Point::new(min_x - px(10.0), min_y - px(10.0)), - Point::new(max_x + px(10.0), max_y + px(10.0)), - ) -} - -fn hit_test_edge(mouse: Point, geom: &EdgeGeometry) -> bool { - let points = sample_bezier(geom, 20); - - for segment in points.windows(2) { - let d = distance_to_segment(mouse, segment[0], segment[1]); - - if d < 8.0 { - return true; - } - } - - false -} - -fn sample_bezier(geom: &EdgeGeometry, steps: usize) -> Vec> { - let mut points = Vec::new(); - - for i in 0..=steps { - let t = i as f32 / steps as f32; - - let x = (1.0 - t).powi(3) * geom.start.x - + 3.0 * (1.0 - t).powi(2) * t * geom.c1.x - + 3.0 * (1.0 - t) * t * t * geom.c2.x - + t.powi(3) * geom.end.x; - - let y = (1.0 - t).powi(3) * geom.start.y - + 3.0 * (1.0 - t).powi(2) * t * geom.c1.y - + 3.0 * (1.0 - t) * t * t * geom.c2.y - + t.powi(3) * geom.end.y; - - points.push(Point::new(x, y)); - } - - points -} -pub fn distance_to_segment(p: Point, a: Point, b: Point) -> f32 { - let ap = vec_sub(p, a); - let ab = vec_sub(b, a); - - let ab_len2 = ab.0 * ab.0 + ab.1 * ab.1; - - if ab_len2 == 0.0 { - return vec_length(ap); - } - - let t = (vec_dot(ap, ab) / ab_len2).clamp(0.0, 1.0); - - let closest = Point::new(f32::from(a.x) + ab.0 * t, f32::from(a.y) + ab.1 * t); - - let dx = f32::from(p.x) - closest.x; - let dy = f32::from(p.y) - closest.y; - - (dx * dx + dy * dy).sqrt() -} - -fn vec_sub(a: Point, b: Point) -> (f32, f32) { - (f32::from(a.x - b.x), f32::from(a.y - b.y)) -} - -fn vec_dot(a: (f32, f32), b: (f32, f32)) -> f32 { - a.0 * b.0 + a.1 * b.1 -} - -fn vec_length(v: (f32, f32)) -> f32 { - (v.0 * v.0 + v.1 * v.1).sqrt() -} diff --git a/crates/ferrum-flow/src/plugins/fit_all.rs b/crates/ferrum-flow/src/plugins/fit_all.rs deleted file mode 100644 index c07f8ab0bb..0000000000 --- a/crates/ferrum-flow/src/plugins/fit_all.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gpui::{Bounds, Point, px}; - -use crate::{ - Node, - plugin::{FlowEvent, InitPluginContext, Plugin, PluginContext, primary_platform_modifier}, - plugins::viewport_frame::{apply_frame_world_rect_direct, frame_world_rect}, -}; - -/// Zoom and pan so **all** nodes fit in the window (⌘0 / Ctrl+0). Undo restores the previous view. -/// -/// On [`Plugin::setup`], fits once using [`InitPluginContext::drawable_size`] (does not push an undo -/// entry; the initial view is not recorded as a command). -pub struct FitAllGraphPlugin; - -impl FitAllGraphPlugin { - pub fn new() -> Self { - Self - } -} - -impl Default for FitAllGraphPlugin { - fn default() -> Self { - Self::new() - } -} - -fn graph_world_bounds_graph<'a>( - nodes: impl Iterator + 'a, -) -> Option<(f32, f32, f32, f32)> { - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - let mut any = false; - - for n in nodes { - let (nx, ny) = n.position(); - let size = *n.size_ref(); - let x: f32 = nx.into(); - let y: f32 = ny.into(); - let w: f32 = size.width.into(); - let h: f32 = size.height.into(); - min_x = min_x.min(x); - min_y = min_y.min(y); - max_x = max_x.max(x + w); - max_y = max_y.max(y + h); - any = true; - } - - if !any { - return None; - } - - Some(( - min_x, - min_y, - (max_x - min_x).max(1.0), - (max_y - min_y).max(1.0), - )) -} - -fn graph_world_bounds(ctx: &PluginContext) -> Option<(f32, f32, f32, f32)> { - graph_world_bounds_graph(ctx.nodes().values()) -} - -fn fit_all(ctx: &mut PluginContext) { - let Some((bx, by, bw, bh)) = graph_world_bounds(ctx) else { - return; - }; - frame_world_rect(ctx, bx, by, bw, bh); -} - -pub(crate) fn fit_entire_graph(ctx: &mut PluginContext) { - fit_all(ctx); -} - -impl Plugin for FitAllGraphPlugin { - fn name(&self) -> &'static str { - "fit_all_graph" - } - - fn setup(&mut self, ctx: &mut InitPluginContext) { - let Some((bx, by, bw, bh)) = graph_world_bounds_graph(ctx.nodes().values()) else { - return; - }; - let win_w: f32 = ctx.drawable_size.width.into(); - let win_h: f32 = ctx.drawable_size.height.into(); - apply_frame_world_rect_direct(ctx, win_w, win_h, bx, by, bw, bh); - ctx.set_window_bounds(Some(Bounds::new( - Point::new(px(0.0), px(0.0)), - ctx.drawable_size, - ))); - } - - fn priority(&self) -> i32 { - 88 - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event - && primary_platform_modifier(ev) - && !ev.keystroke.modifiers.shift - && ev.keystroke.key == "0" - { - fit_all(ctx); - return crate::plugin::EventResult::Stop; - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/focus_selection.rs b/crates/ferrum-flow/src/plugins/focus_selection.rs deleted file mode 100644 index 5cc2f6bddd..0000000000 --- a/crates/ferrum-flow/src/plugins/focus_selection.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{ - plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier}, - plugins::viewport_frame::frame_world_rect, -}; - -/// Pan + zoom the viewport so selected nodes fit the window (⌘⇧F / Ctrl⇧F). Undo restores prior view. -pub struct FocusSelectionPlugin; - -impl FocusSelectionPlugin { - pub fn new() -> Self { - Self - } -} - -impl Default for FocusSelectionPlugin { - fn default() -> Self { - Self::new() - } -} - -fn focus_shortcut(ev: &gpui::KeyDownEvent) -> bool { - primary_platform_modifier(ev) && ev.keystroke.modifiers.shift -} - -fn focus_selected(ctx: &mut PluginContext) { - let Some(bounds) = ctx.graph.selection_bounds() else { - return; - }; - let bx: f32 = bounds.origin.x.into(); - let by: f32 = bounds.origin.y.into(); - let bw: f32 = bounds.size.width.into(); - let bh: f32 = bounds.size.height.into(); - frame_world_rect(ctx, bx, by, bw, bh); -} - -pub(crate) fn focus_viewport_on_selection(ctx: &mut PluginContext) { - focus_selected(ctx); -} - -impl Plugin for FocusSelectionPlugin { - fn name(&self) -> &'static str { - "focus_selection" - } - - fn priority(&self) -> i32 { - 90 - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event - && focus_shortcut(ev) - && ev.keystroke.key == "f" - { - focus_selected(ctx); - return crate::plugin::EventResult::Stop; - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/history.rs b/crates/ferrum-flow/src/plugins/history.rs deleted file mode 100644 index a88ec5ce9c..0000000000 --- a/crates/ferrum-flow/src/plugins/history.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::plugin::{FlowEvent, Plugin, primary_platform_modifier}; - -pub struct HistoryPlugin; - -impl HistoryPlugin { - pub fn new() -> Self { - Self {} - } -} - -impl Default for HistoryPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for HistoryPlugin { - fn name(&self) -> &'static str { - "history" - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event { - let primary = primary_platform_modifier(ev); - if ev.keystroke.key == "z" && primary && ev.keystroke.modifiers.shift { - ctx.redo(); - return crate::plugin::EventResult::Stop; - } else if ev.keystroke.key == "z" && primary { - ctx.undo(); - return crate::plugin::EventResult::Stop; - } - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/minimap.rs b/crates/ferrum-flow/src/plugins/minimap.rs deleted file mode 100644 index 9d41a735b2..0000000000 --- a/crates/ferrum-flow/src/plugins/minimap.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! Overview minimap: full-graph bounds in world space, current viewport indicator, click-to-center. - -use std::collections::HashMap; - -use gpui::{ - Bounds, Element, MouseButton, PathBuilder, Pixels, Point, Size, Styled as _, canvas, px, rgb, -}; - -use crate::{ - NodeId, Viewport, - canvas::{Command, CommandContext}, - plugin::{ - EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer, - }, -}; - -const MAP_W: f32 = 200.0; -const MAP_H: f32 = 140.0; -const OUTER_MARGIN: f32 = 16.0; -const INNER_INSET: f32 = 3.0; -const WORLD_PAD: f32 = 96.0; - -/// Warm-start capacity for the viewport-visible node map; typical sessions stay in low hundreds. -/// Capped by total node count so tiny graphs do not over-allocate. -const VISIBLE_NODE_MAP_CAPACITY_HINT: usize = 128; - -/// Last-computed layout for hit-testing (updated each [`MinimapPlugin::render`]). -#[derive(Clone)] -struct MinimapLayout { - chrome: Bounds, - inner: Bounds, - world_x0: f32, - world_y0: f32, - world_w: f32, - world_h: f32, -} - -impl MinimapLayout { - fn contains_chrome(&self, p: Point) -> bool { - self.chrome.contains(&p) - } - - /// Maps a screen position inside the chrome to world coordinates (clamped to the mapped extent). - fn screen_to_world(&self, screen: Point) -> Point { - let ix: f32 = self.inner.origin.x.into(); - let iy: f32 = self.inner.origin.y.into(); - let iw: f32 = self.inner.size.width.into(); - let ih: f32 = self.inner.size.height.into(); - let sx: f32 = screen.x.into(); - let sy: f32 = screen.y.into(); - let u = ((sx - ix) / iw.max(1.0)).clamp(0.0, 1.0); - let v = ((sy - iy) / ih.max(1.0)).clamp(0.0, 1.0); - let wx = self.world_x0 + u * self.world_w; - let wy = self.world_y0 + v * self.world_h; - Point::new(px(wx), px(wy)) - } -} - -fn graph_world_extent(ctx: &RenderContext) -> (f32, f32, f32, f32) { - let nodes: Vec<_> = ctx - .graph - .nodes() - .values() - .filter(|n| ctx.is_node_visible_node(n)) - .collect(); - if nodes.is_empty() { - return (0.0, 0.0, 640.0, 480.0); - } - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - for n in nodes { - let (nx, ny) = n.position(); - let size = *n.size_ref(); - let x: f32 = nx.into(); - let y: f32 = ny.into(); - let w: f32 = size.width.into(); - let h: f32 = size.height.into(); - min_x = min_x.min(x); - min_y = min_y.min(y); - max_x = max_x.max(x + w); - max_y = max_y.max(y + h); - } - let w = (max_x - min_x + 2.0 * WORLD_PAD).max(120.0); - let h = (max_y - min_y + 2.0 * WORLD_PAD).max(120.0); - (min_x - WORLD_PAD, min_y - WORLD_PAD, w, h) -} - -fn visible_world_aabb(viewport: &Viewport, win: &Bounds) -> (f32, f32, f32, f32) { - let w: f32 = win.size.width.into(); - let h: f32 = win.size.height.into(); - let corners = [ - viewport.screen_to_world(Point::new(px(0.0), px(0.0))), - viewport.screen_to_world(Point::new(px(w), px(0.0))), - viewport.screen_to_world(Point::new(px(w), px(h))), - viewport.screen_to_world(Point::new(px(0.0), px(h))), - ]; - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - for c in corners { - let x: f32 = c.x.into(); - let y: f32 = c.y.into(); - min_x = min_x.min(x); - min_y = min_y.min(y); - max_x = max_x.max(x); - max_y = max_y.max(y); - } - ( - min_x, - min_y, - (max_x - min_x).max(1.0), - (max_y - min_y).max(1.0), - ) -} - -fn build_layout(ctx: &RenderContext) -> Option { - let win = ctx.window_bounds()?; - let ww: f32 = win.size.width.into(); - let wh: f32 = win.size.height.into(); - if ww < MAP_W + OUTER_MARGIN || wh < MAP_H + OUTER_MARGIN { - return None; - } - - let map_w = px(MAP_W); - let map_h = px(MAP_H); - let ox = win.size.width - map_w - px(OUTER_MARGIN); - let oy = win.size.height - map_h - px(OUTER_MARGIN); - let chrome = Bounds::new(Point::new(ox, oy), Size::new(map_w, map_h)); - - let inset = px(INNER_INSET); - let inner = Bounds::new( - chrome.origin + Point::new(inset, inset), - Size::new( - chrome.size.width - inset * 2.0, - chrome.size.height - inset * 2.0, - ), - ); - - let (wx0, wy0, ww, wh) = graph_world_extent(ctx); - - Some(MinimapLayout { - chrome, - inner, - world_x0: wx0, - world_y0: wy0, - world_w: ww.max(1.0), - world_h: wh.max(1.0), - }) -} - -fn world_to_inner_pt(wx: f32, wy: f32, layout: &MinimapLayout) -> Point { - let u = ((wx - layout.world_x0) / layout.world_w).clamp(0.0, 1.0); - let v = ((wy - layout.world_y0) / layout.world_h).clamp(0.0, 1.0); - let ix: f32 = layout.inner.origin.x.into(); - let iy: f32 = layout.inner.origin.y.into(); - let iw: f32 = layout.inner.size.width.into(); - let ih: f32 = layout.inner.size.height.into(); - Point::new(px(ix + u * iw), px(iy + v * ih)) -} - -fn center_viewport_on_world(ctx: &mut PluginContext, world: Point) { - let Some(wb) = ctx.window_bounds() else { - return; - }; - let cx: f32 = (wb.size.width / 2.0).into(); - let cy: f32 = (wb.size.height / 2.0).into(); - let z = ctx.zoom(); - let wx: f32 = world.x.into(); - let wy: f32 = world.y.into(); - let from = ctx.offset(); - ctx.set_offset_xy(px(cx - wx * z), px(cy - wy * z)); - let to = ctx.offset(); - ctx.execute_command(MinimapPanCommand { from, to }); -} - -struct MinimapPanCommand { - from: Point, - to: Point, -} - -impl Command for MinimapPanCommand { - fn name(&self) -> &'static str { - "minimap_pan" - } - - fn execute(&mut self, ctx: &mut CommandContext) { - ctx.set_offset(self.to); - } - - fn undo(&mut self, ctx: &mut CommandContext) { - ctx.set_offset(self.from); - } - - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - vec![] - } -} - -/// Renders a bottom-right overview map and pans the viewport when the user clicks it. -/// -/// Uses priority **135** so clicks hit the minimap before [`crate::plugins::SelectionPlugin`] (100) -/// starts a canvas selection. -pub struct MinimapPlugin { - last_layout: Option, -} - -impl MinimapPlugin { - pub fn new() -> Self { - Self { last_layout: None } - } -} - -impl Default for MinimapPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for MinimapPlugin { - fn name(&self) -> &'static str { - "minimap" - } - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event - && let Some(ref layout) = self.last_layout - && layout.contains_chrome(ev.position) - { - if ev.button == MouseButton::Right { - return EventResult::Stop; - } else if ev.button == MouseButton::Left { - let world = layout.screen_to_world(ev.position); - center_viewport_on_world(ctx, world); - ctx.notify(); - return EventResult::Stop; - } - } - EventResult::Continue - } - - fn priority(&self) -> i32 { - 135 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let layout = build_layout(ctx)?; - self.last_layout = Some(layout.clone()); - - let inner = layout.inner; - - // One visibility pass: rects for node quads, and world-space centers for edges (no second - // `get_node` per endpoint). - let map_cap = VISIBLE_NODE_MAP_CAPACITY_HINT.min(ctx.graph.nodes().len()); - let mut visible_centers = HashMap::::with_capacity(map_cap); - let nodes: Vec<_> = ctx - .graph - .nodes() - .values() - .filter_map(|n| { - if !ctx.is_node_visible(&n.id()) { - return None; - } - let (nx, ny) = n.position(); - let size = *n.size_ref(); - let x: f32 = nx.into(); - let y: f32 = ny.into(); - let w: f32 = size.width.into(); - let h: f32 = size.height.into(); - visible_centers.insert(n.id(), (x + w * 0.5, y + h * 0.5)); - Some((x, y, w, h)) - }) - .collect(); - - let edges: Vec<_> = ctx - .graph - .edges_values() - .filter_map(|e| { - let s = ctx.graph.get_port(&e.source_port)?; - let t = ctx.graph.get_port(&e.target_port)?; - let (sx, sy) = visible_centers.get(&s.node_id())?; - let (tx, ty) = visible_centers.get(&t.node_id())?; - Some((*sx, *sy, *tx, *ty)) - }) - .collect(); - - let win_bounds = ctx.window_bounds()?; - let (vx0, vy0, vw, vh) = visible_world_aabb(ctx.viewport(), &win_bounds); - let v_tl = world_to_inner_pt(vx0, vy0, &layout); - let v_br = world_to_inner_pt(vx0 + vw, vy0 + vh, &layout); - - let minimap_background = ctx.theme.minimap_background; - let minimap_border = ctx.theme.minimap_border; - let minimap_edge = ctx.theme.minimap_edge; - let minimap_node_fill = ctx.theme.minimap_node_fill; - let minimap_node_stroke = ctx.theme.minimap_node_stroke; - let minimap_viewport_stroke = ctx.theme.minimap_viewport_stroke; - - Some( - canvas( - move |_, _, _| (), - move |bounds, _, win, _| { - let origin = bounds.origin; - // Inner background - if let Ok(p) = rect_fill_path(offset_bounds(inner, origin)) { - win.paint_path(p, rgb(minimap_background)); - } - if let Ok(p) = rect_stroke_path(offset_bounds(inner, origin), px(1.0)) { - win.paint_path(p, rgb(minimap_border)); - } - - // Edges (straight segments between node centers) - for (sx, sy, tx, ty) in edges { - let a = world_to_inner_pt(sx, sy, &layout); - let b = world_to_inner_pt(tx, ty, &layout); - let mut line = PathBuilder::stroke(px(1.0)); - line.move_to(a + origin); - line.line_to(b + origin); - if let Ok(p) = line.build() { - win.paint_path(p, rgb(minimap_edge)); - } - } - - for (x, y, nw, nh) in nodes { - let p0 = world_to_inner_pt(x, y, &layout); - let p1 = world_to_inner_pt(x + nw, y + nh, &layout); - let min_x = f32::min(f32::from(p0.x), f32::from(p1.x)); - let max_x = f32::max(f32::from(p0.x), f32::from(p1.x)); - let min_y = f32::min(f32::from(p0.y), f32::from(p1.y)); - let max_y = f32::max(f32::from(p0.y), f32::from(p1.y)); - let rw = (max_x - min_x).max(2.0); - let rh = (max_y - min_y).max(2.0); - let o = Point::new(px(min_x), px(min_y)) + origin; - let s = Size::new(px(rw), px(rh)); - if let Ok(p) = rect_fill_bounds(o, s) { - win.paint_path(p, rgb(minimap_node_fill)); - } - if let Ok(p) = rect_stroke_bounds(o, s, px(1.0)) { - win.paint_path(p, rgb(minimap_node_stroke)); - } - } - - // Viewport frame - let min_x = f32::min(f32::from(v_tl.x), f32::from(v_br.x)); - let max_x = f32::max(f32::from(v_tl.x), f32::from(v_br.x)); - let min_y = f32::min(f32::from(v_tl.y), f32::from(v_br.y)); - let max_y = f32::max(f32::from(v_tl.y), f32::from(v_br.y)); - let vo = Point::new(px(min_x), px(min_y)) + origin; - let vs = Size::new(px((max_x - min_x).max(2.0)), px((max_y - min_y).max(2.0))); - if let Ok(p) = rect_stroke_bounds(vo, vs, px(1.5)) { - win.paint_path(p, rgb(minimap_viewport_stroke)); - } - }, - ) - .absolute() - .size_full() - .into_any(), - ) - } -} - -fn offset_bounds(bounds: Bounds, offset: Point) -> Bounds { - Bounds::new(bounds.origin + offset, bounds.size) -} - -fn rect_fill_path(b: Bounds) -> Result, anyhow::Error> { - rect_fill_bounds(b.origin, b.size) -} - -fn rect_fill_bounds( - o: Point, - s: Size, -) -> Result, anyhow::Error> { - let x0: f32 = o.x.into(); - let y0: f32 = o.y.into(); - let w: f32 = s.width.into(); - let h: f32 = s.height.into(); - let pts = [ - Point::new(px(x0), px(y0)), - Point::new(px(x0 + w), px(y0)), - Point::new(px(x0 + w), px(y0 + h)), - Point::new(px(x0), px(y0 + h)), - ]; - let mut pb = PathBuilder::fill(); - pb.add_polygon(&pts, true); - pb.build() -} - -fn rect_stroke_path(b: Bounds, width: Pixels) -> Result, anyhow::Error> { - rect_stroke_bounds(b.origin, b.size, width) -} - -fn rect_stroke_bounds( - o: Point, - s: Size, - width: Pixels, -) -> Result, anyhow::Error> { - let x0: f32 = o.x.into(); - let y0: f32 = o.y.into(); - let w: f32 = s.width.into(); - let h: f32 = s.height.into(); - let mut line = PathBuilder::stroke(width); - line.move_to(Point::new(px(x0), px(y0))); - line.line_to(Point::new(px(x0 + w), px(y0))); - line.line_to(Point::new(px(x0 + w), px(y0 + h))); - line.line_to(Point::new(px(x0), px(y0 + h))); - line.close(); - line.build() -} - -#[cfg(test)] -mod command_interop_tests { - use gpui::{Point, px}; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::MinimapPanCommand; - - #[test] - fn minimap_pan_command_interop() { - let base = Graph::new(); - let cmd = MinimapPanCommand { - from: Point::new(px(1.0), px(2.0)), - to: Point::new(px(10.0), px(20.0)), - }; - assert_command_interop( - &base, - || { - Box::new(MinimapPanCommand { - from: cmd.from, - to: cmd.to, - }) - }, - "MinimapPanCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/mod.rs b/crates/ferrum-flow/src/plugins/mod.rs deleted file mode 100644 index 12e43dd563..0000000000 --- a/crates/ferrum-flow/src/plugins/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -mod align; -mod background; -mod clipboard; -mod context_menu; -mod delete; -mod edge; -mod fit_all; -mod focus_selection; -mod history; -mod minimap; -mod node; -mod port; -mod select_all_viewport; -mod selection; -mod snap_guides; -mod toast; -mod viewport; -mod viewport_frame; -mod zoom_controls; - -pub use align::AlignPlugin; -pub use background::BackgroundPlugin; -pub use clipboard::ClipboardPlugin; -pub use context_menu::{ContextMenuCanvasExtra, ContextMenuCustomAction, ContextMenuPlugin}; -pub use delete::DeletePlugin; -pub use edge::EdgePlugin; -pub use fit_all::FitAllGraphPlugin; -pub use focus_selection::FocusSelectionPlugin; -pub use history::HistoryPlugin; -pub use minimap::MinimapPlugin; -pub use node::{ - ActiveNodeDrag, NODE_DRAG_TICK_INTERVAL, NodeDragEvent, NodeInteractionPlugin, NodePlugin, -}; -pub use port::{ - CreateEdge, CreateNode, CreatePort, DefaultEdgeValidator, EdgeValidationError, - EdgeValidationErrorCode, EdgeValidator, PortInteractionPlugin, edge_bezier, filled_disc_path, - port_screen_big_bounds, port_screen_bounds, -}; -pub use select_all_viewport::SelectAllViewportPlugin; -pub use selection::SelectionPlugin; -pub use snap_guides::SnapGuidesPlugin; -pub use toast::{ToastLevel, ToastMessage, ToastPlugin}; -pub use viewport::ViewportPlugin; -pub use zoom_controls::ZoomControlsPlugin; diff --git a/crates/ferrum-flow/src/plugins/node/command.rs b/crates/ferrum-flow/src/plugins/node/command.rs deleted file mode 100644 index 38e64dc0b2..0000000000 --- a/crates/ferrum-flow/src/plugins/node/command.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::collections::HashSet; - -use gpui::{Pixels, Point}; - -use crate::{EdgeId, GraphOp, NodeId, canvas::Command, plugin::PluginContext}; - -pub struct SelecteNodeCommand { - node_id: NodeId, - shift: bool, - old_node_order: Vec, - old_selected_edge: HashSet, - old_selected_node: HashSet, -} - -impl SelecteNodeCommand { - pub fn new(node_id: NodeId, shift: bool, ctx: &PluginContext) -> Self { - Self { - node_id, - shift, - old_node_order: ctx.graph.node_order().clone(), - old_selected_edge: ctx.graph.selected_edge().clone(), - old_selected_node: ctx.graph.selected_node().clone(), - } - } -} - -impl Command for SelecteNodeCommand { - fn name(&self) -> &'static str { - "select_node" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - if !self.shift { - ctx.clear_selected_edge(); - } - ctx.add_selected_node(self.node_id, self.shift); - ctx.bring_node_to_front(self.node_id); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.graph.set_selected_node(self.old_selected_node.clone()); - ctx.graph.set_selected_edge(self.old_selected_edge.clone()); - let a = ctx.graph.node_order_mut(); - *a = self.old_node_order.clone(); - } - - fn to_ops(&self, ctx: &mut crate::CommandContext) -> Vec { - if !self.shift { - ctx.clear_selected_edge(); - } - ctx.add_selected_node(self.node_id, self.shift); - - let mut list = vec![]; - let index = ctx - .graph - .node_order() - .iter() - .position(|v| *v == self.node_id); - if let Some(index) = index { - list.push(GraphOp::NodeOrderRemove { index }) - } - list.push(GraphOp::NodeOrderInsert { id: self.node_id }); - list - } -} - -pub struct DragNodesCommand { - from: Vec<(NodeId, Point)>, - to: Vec<(NodeId, Point)>, -} - -impl DragNodesCommand { - pub fn new(start_positions: &[(NodeId, Point)], ctx: &PluginContext) -> Self { - let mut to = Vec::new(); - for (node_id, _) in start_positions { - if let Some(node) = ctx.get_node(node_id) { - to.push((*node_id, node.point())); - } - } - Self { - from: start_positions.to_vec(), - to, - } - } - - /// Explicit before/after positions (same node order, same length). Use for align / distribute. - pub fn from_positions( - from: Vec<(NodeId, Point)>, - to: Vec<(NodeId, Point)>, - ) -> Self { - Self { from, to } - } -} - -impl Command for DragNodesCommand { - fn name(&self) -> &'static str { - "drag_nodes" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - for (id, point) in self.to.iter() { - if let Some(node) = ctx.get_node_mut(id) { - node.set_position_with_point(*point); - } - } - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - for (id, point) in self.from.iter() { - if let Some(node) = ctx.get_node_mut(id) { - node.set_position_with_point(*point); - } - } - } - - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - let mut list = vec![]; - for (id, point) in self.to.iter() { - list.push(GraphOp::MoveNode { - id: *id, - x: Into::::into(point.x), - y: Into::::into(point.y), - }) - } - - vec![GraphOp::Batch(list)] - } -} - -#[cfg(test)] -mod command_interop_tests { - use gpui::{Point, px}; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::{DragNodesCommand, SelecteNodeCommand}; - - #[test] - fn select_node_command_interop() { - let mut base = Graph::new(); - let n1 = base.create_node("a").position(0.0, 0.0).build().unwrap(); - let _n2 = base.create_node("b").position(50.0, 0.0).build().unwrap(); - - let old_node_order = base.node_order().to_vec(); - let old_selected_edge = base.selected_edge().clone(); - let old_selected_node = base.selected_node().clone(); - - assert_command_interop( - &base, - || { - Box::new(SelecteNodeCommand { - node_id: n1, - shift: false, - old_node_order: old_node_order.clone(), - old_selected_edge: old_selected_edge.clone(), - old_selected_node: old_selected_node.clone(), - }) - }, - "SelecteNodeCommand", - ); - } - - #[test] - fn drag_nodes_command_interop() { - let mut base = Graph::new(); - let n = base.create_node("n").position(0.0, 0.0).build().unwrap(); - let from = vec![(n, Point::new(px(0.0), px(0.0)))]; - let to = vec![(n, Point::new(px(30.0), px(40.0)))]; - let cmd = DragNodesCommand::from_positions(from, to); - - assert_command_interop( - &base, - || { - Box::new(DragNodesCommand::from_positions( - cmd.from.clone(), - cmd.to.clone(), - )) - }, - "DragNodesCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/node/drag_events.rs b/crates/ferrum-flow/src/plugins/node/drag_events.rs deleted file mode 100644 index 6bee31a6d2..0000000000 --- a/crates/ferrum-flow/src/plugins/node/drag_events.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Custom [`FlowEvent`](crate::plugin::FlowEvent) payloads for primary (left-button) node dragging. -//! Emitted by [`super::interaction::NodeDragInteraction`]. Other plugins (e.g. snap guides) may -//! subscribe via [`FlowEvent::as_custom`](crate::plugin::FlowEvent::as_custom). -//! -//! [`NodeDragEvent::Tick`] carries [`std::sync::Arc`] so the emitter can share the same id list across -//! ticks without reallocating (custom events cannot borrow interaction state). - -use std::sync::Arc; -use std::time::Duration; - -use crate::NodeId; - -/// Stored in [`crate::SharedState`] while [`super::interaction::NodeDragInteraction`] is active in -/// the dragging phase: these node ids are rendered on the interaction layer only; [`super::NodePlugin`] -/// skips them in the static nodes layer to cut work per frame. -#[derive(Clone, Debug)] -pub struct ActiveNodeDrag(pub Arc<[NodeId]>); - -/// Default throttle for [`NodeDragEvent::Tick`] ([`crate::plugins::NodeInteractionPlugin::new`]). -/// Use [`crate::plugins::NodeInteractionPlugin::with_drag_tick_interval`] to change it. -pub const NODE_DRAG_TICK_INTERVAL: Duration = Duration::from_millis(50); - -/// Primary node drag lifecycle on the canvas (left-button drag from [`super::NodeInteractionPlugin`]). -#[derive(Debug, Clone)] -pub enum NodeDragEvent { - /// Throttled while dragging; [`crate::Graph`] already holds updated positions for these nodes. - /// Same slice is reused for the whole drag (cheap [`Arc::clone`] per tick). - Tick(Arc<[NodeId]>), - /// Drag finished: click without move, or pointer released after a drag. - End, -} diff --git a/crates/ferrum-flow/src/plugins/node/interaction.rs b/crates/ferrum-flow/src/plugins/node/interaction.rs deleted file mode 100644 index 3fe9dc69a4..0000000000 --- a/crates/ferrum-flow/src/plugins/node/interaction.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use gpui::{MouseButton, Pixels, Point, px}; - -use crate::{ - NodeId, - canvas::{Interaction, InteractionResult}, - plugin::{EventResult, FlowEvent, InputEvent, Plugin, PluginContext}, - plugins::node::{ - ActiveNodeDrag, NODE_DRAG_TICK_INTERVAL, NodeDragEvent, - command::{DragNodesCommand, SelecteNodeCommand}, - }, -}; - -const DRAG_THRESHOLD: Pixels = px(2.0); -const DRAG_COMMAND_INTERVAL: Duration = Duration::from_millis(50); - -/// Configures [`NodeDragInteraction`] sampling for [`NodeDragEvent::Tick`]. -pub struct NodeInteractionPlugin { - drag_tick_interval: Duration, -} - -impl NodeInteractionPlugin { - pub fn new() -> Self { - Self { - drag_tick_interval: NODE_DRAG_TICK_INTERVAL, - } - } - - /// Override the drag tick interval (e.g. lower for snappier alignment feedback, higher to reduce load). - pub fn with_drag_tick_interval(interval: Duration) -> Self { - Self { - drag_tick_interval: interval, - } - } -} - -impl Default for NodeInteractionPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for NodeInteractionPlugin { - fn name(&self) -> &'static str { - "node_interaction" - } - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event { - if ev.button != MouseButton::Left { - return EventResult::Continue; - } - let mouse_world = ctx.screen_to_world(ev.position); - - if let Some(node_id) = ctx.hit_node(mouse_world) { - ctx.start_interaction(NodeDragInteraction::start( - node_id, - mouse_world, - ev.modifiers.shift, - self.drag_tick_interval, - )); - - return EventResult::Stop; - } else { - ctx.clear_selected_node(); - } - } - - EventResult::Continue - } - - fn priority(&self) -> i32 { - 120 - } -} - -pub struct NodeDragInteraction { - state: NodeDragState, - drag_tick_interval: Duration, - last_drag_command_at: Option, - last_node_drag_tick_at: Option, -} - -enum NodeDragState { - Pending { - node_id: NodeId, - start_mouse: Point, - shift: bool, - }, - Draging { - start_mouse: Point, - start_positions: Vec<(NodeId, Point)>, - /// Stable for this drag; cheap to [`Arc::clone`] into each [`NodeDragEvent::Tick`]. - dragged_ids: Arc<[NodeId]>, - }, -} - -impl NodeDragInteraction { - fn start( - node_id: NodeId, - start_mouse: Point, - shift: bool, - drag_tick_interval: Duration, - ) -> Self { - Self { - state: NodeDragState::Pending { - node_id, - start_mouse, - shift, - }, - drag_tick_interval, - last_drag_command_at: None, - last_node_drag_tick_at: None, - } - } -} - -impl Interaction for NodeDragInteraction { - fn on_mouse_move( - &mut self, - ev: &gpui::MouseMoveEvent, - ctx: &mut PluginContext, - ) -> crate::canvas::InteractionResult { - match &self.state { - NodeDragState::Pending { - node_id, - start_mouse, - .. - } => { - let delta = ctx.screen_to_world(ev.position) - *start_mouse; - if delta.x.abs() > DRAG_THRESHOLD || delta.y.abs() > DRAG_THRESHOLD { - let mut nodes = vec![]; - - if ctx.graph.selected_node().contains(node_id) { - for id in ctx.graph.selected_node() { - if let Some(node) = ctx.nodes().get(id) { - nodes.push((*id, node.point())); - } - } - } else if let Some(node) = ctx.nodes().get(node_id) { - nodes.push((*node_id, node.point())); - } - let dragged_ids: Arc<[NodeId]> = - nodes.iter().map(|(id, _)| *id).collect::>().into(); - self.state = NodeDragState::Draging { - start_mouse: ev.position, - start_positions: nodes, - dragged_ids: Arc::clone(&dragged_ids), - }; - ctx.shared_state.insert(ActiveNodeDrag(dragged_ids)); - - ctx.notify(); - } - } - NodeDragState::Draging { - start_mouse, - start_positions, - dragged_ids, - } => { - let dx = ctx.screen_length_to_world(ev.position.x - start_mouse.x); - let dy = ctx.screen_length_to_world(ev.position.y - start_mouse.y); - for (id, point) in start_positions.iter() { - if let Some(node) = ctx.get_node_mut(id) { - node.set_position(point.x + dx, point.y + dy); - } - } - - let now = Instant::now(); - - if ctx.has_sync_plugin() { - let should_command = self - .last_drag_command_at - .map(|t| now.duration_since(t) >= DRAG_COMMAND_INTERVAL) - .unwrap_or(true); - if should_command { - ctx.execute_command(DragNodesCommand::new(start_positions, ctx)); - self.last_drag_command_at = Some(now); - } - } - - let should_tick = self - .last_node_drag_tick_at - .map(|t| now.duration_since(t) >= self.drag_tick_interval) - .unwrap_or(true); - if should_tick { - self.last_node_drag_tick_at = Some(now); - ctx.emit(FlowEvent::custom(NodeDragEvent::Tick(Arc::clone( - dragged_ids, - )))); - } else { - ctx.notify(); - } - } - } - InteractionResult::Continue - } - fn on_mouse_up( - &mut self, - _ev: &gpui::MouseUpEvent, - ctx: &mut PluginContext, - ) -> crate::canvas::InteractionResult { - ctx.shared_state.remove::(); - match &self.state { - NodeDragState::Pending { node_id, shift, .. } => { - ctx.emit(FlowEvent::custom(NodeDragEvent::End)); - ctx.execute_command(SelecteNodeCommand::new(*node_id, *shift, ctx)); - InteractionResult::End - } - NodeDragState::Draging { - start_positions, .. - } => { - ctx.emit(FlowEvent::custom(NodeDragEvent::End)); - ctx.execute_command(DragNodesCommand::new(start_positions, ctx)); - InteractionResult::End - } - } - } - fn render(&self, ctx: &mut crate::plugin::RenderContext) -> Option { - match &self.state { - NodeDragState::Draging { dragged_ids, .. } => Some(super::render_node_cards( - ctx, - dragged_ids.as_ref(), - "draging-node-cards", - )), - NodeDragState::Pending { .. } => None, - } - } -} diff --git a/crates/ferrum-flow/src/plugins/node/mod.rs b/crates/ferrum-flow/src/plugins/node/mod.rs deleted file mode 100644 index 0bfc77d29f..0000000000 --- a/crates/ferrum-flow/src/plugins/node/mod.rs +++ /dev/null @@ -1,131 +0,0 @@ -mod command; -mod drag_events; -mod interaction; - -pub use command::DragNodesCommand; -pub use drag_events::{ActiveNodeDrag, NODE_DRAG_TICK_INTERVAL, NodeDragEvent}; -use gpui::{Element as _, ElementId, InteractiveElement as _, ParentElement, div}; -pub use interaction::NodeInteractionPlugin; - -/// Renders the given nodes (and their ports) like [`NodePlugin`], for use on the interaction overlay. -pub(super) fn render_node_cards( - ctx: &mut RenderContext, - node_ids: &[crate::NodeId], - id: &'static str, -) -> gpui::AnyElement { - ctx.cache_port_offset_with_nodes(node_ids); - let list = node_ids.iter().filter_map(|node_id| { - let node = ctx.graph.nodes().get(node_id)?; - let render = ctx.renderers.get(node.renderer_key()); - - let node_render = render.render(node, ctx); - - let port_ids: Vec = ctx.cached_port_ids_for_node(node_id).collect(); - let ports = port_ids.iter().filter_map(|port_id| { - let port = ctx.graph.get_port(port_id)?; - render.port_render(node, port, ctx) - }); - - Some( - div() - .id(ElementId::Uuid(*node_id.as_uuid())) - .child(node_render) - .children(ports), - ) - }); - - div().id(id).children(list).into_any() -} - -use std::sync::Arc; - -use crate::NodeId; -use crate::plugin::{Plugin, RenderContext}; -use crate::viewport::ViewportVisibilityCacheKey; - -/// Invalidates [`NodePlugin::static_layer_node_ids`] when the viewport changes **or** the active -/// node-drag overlay set changes ([`ActiveNodeDrag`] `Arc` identity + length). -#[derive(Clone, Copy, Debug, PartialEq)] -struct NodeStaticLayerCacheKey { - viewport: ViewportVisibilityCacheKey, - nodes_len: usize, - node_order_len: usize, - node_order_tail: Option, - /// `None` when not dragging; else [`Arc::as_ptr`] + len of the shared drag id list. - drag_arc: Option<(usize, usize)>, -} - -impl NodeStaticLayerCacheKey { - fn from_render_ctx(ctx: &RenderContext) -> Self { - let drag = ctx.get_shared_state::(); - Self { - viewport: ctx.viewport().visibility_cache_key(), - nodes_len: ctx.graph.nodes().len(), - node_order_len: ctx.graph.node_order().len(), - node_order_tail: ctx - .graph - .node_order() - .last() - .map(|id| id.as_uuid().as_u128()), - drag_arc: drag.map(|d| { - let p = Arc::as_ptr(&d.0); - (p.cast::() as usize, d.0.len()) - }), - } - } -} - -pub struct NodePlugin { - static_layer_cache_key: Option, - /// Viewport-visible nodes for the static [`RenderLayer::Nodes`] layer, already excluding - /// [`ActiveNodeDrag`] ids (those render on the interaction overlay). - static_layer_node_ids: Vec, -} - -impl NodePlugin { - pub fn new() -> Self { - Self { - static_layer_cache_key: None, - static_layer_node_ids: Vec::new(), - } - } -} - -impl Default for NodePlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for NodePlugin { - fn name(&self) -> &'static str { - "node" - } - fn priority(&self) -> i32 { - 60 - } - fn render_layer(&self) -> crate::plugin::RenderLayer { - crate::plugin::RenderLayer::Nodes - } - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let key = NodeStaticLayerCacheKey::from_render_ctx(ctx); - if self.static_layer_cache_key != Some(key) { - self.static_layer_cache_key = Some(key); - let active = ctx.get_shared_state::(); - self.static_layer_node_ids = ctx - .graph - .node_order() - .iter() - .filter(|node_id| ctx.is_node_visible(node_id)) - .filter(|node_id| !active.is_some_and(|d| d.0.contains(node_id))) - .copied() - .collect(); - } - - Some(render_node_cards( - ctx, - &self.static_layer_node_ids, - "static-layer-node-cards", - )) - } -} diff --git a/crates/ferrum-flow/src/plugins/port/command.rs b/crates/ferrum-flow/src/plugins/port/command.rs deleted file mode 100644 index fed682bfec..0000000000 --- a/crates/ferrum-flow/src/plugins/port/command.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::{Edge, GraphOp, Node, Port, canvas::Command}; - -pub struct CreateEdge { - edge: Edge, -} - -impl CreateEdge { - pub fn new(edge: Edge) -> Self { - Self { edge } - } -} - -impl Command for CreateEdge { - fn name(&self) -> &'static str { - "create_edge" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.add_edge(self.edge.clone()); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.remove_edge(&self.edge.id); - } - - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - vec![GraphOp::AddEdge(self.edge.clone())] - } -} - -pub struct CreateNode { - node: Node, -} - -impl CreateNode { - pub fn new(node: Node) -> Self { - Self { node } - } -} - -impl Command for CreateNode { - fn name(&self) -> &'static str { - "create_node" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.add_node(self.node.clone()); - } - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - vec![ - GraphOp::AddNode(self.node.clone()), - GraphOp::NodeOrderInsert { id: self.node.id() }, - ] - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.remove_node(&self.node.id()); - } -} - -pub struct CreatePort { - port: Port, -} - -impl CreatePort { - pub fn new(port: Port) -> Self { - Self { port } - } -} - -impl Command for CreatePort { - fn name(&self) -> &'static str { - "create_port" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.add_port(self.port.clone()); - ctx.port_offset_cache.clear_node(&self.port.node_id()); - } - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - vec![GraphOp::AddPort(self.port.clone())] - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - let node_id = self.port.node_id(); - ctx.remove_port(&self.port.id()); - ctx.port_offset_cache.clear_node(&node_id); - } -} - -#[cfg(test)] -mod command_interop_tests { - use serde_json::json; - - use crate::{ - CreateEdge, CreateNode, CreatePort, Graph, PortBuilder, PortKind, PortPosition, PortType, - command_interop::assert_command_interop, - }; - - #[test] - fn create_node_command_interop() { - let mut base = Graph::new(); - let (node, _ports, _) = base - .create_node("x") - .position(100.0, 80.0) - .data(json!({ "k": "v" })) - .build_raw(); - - assert_command_interop( - &base, - || Box::new(CreateNode::new(node.clone())), - "CreateNode", - ); - } - - #[test] - fn create_port_command_interop() { - let mut base = Graph::new(); - let node_id = base.create_node("x").position(0.0, 0.0).build().unwrap(); - let port = PortBuilder::new(base.next_port_id()) - .kind(PortKind::Output) - .node_id(node_id) - .index(0) - .position(PortPosition::Right) - .size(12.0, 12.0) - .port_type(PortType::Any) - .build(); - - assert_command_interop( - &base, - || Box::new(CreatePort::new(port.clone())), - "CreatePort", - ); - } - - #[test] - fn create_edge_command_interop() { - let mut base = Graph::new(); - let n1 = base - .create_node("a") - .position(0.0, 0.0) - .output() - .build() - .unwrap(); - let n2 = base - .create_node("b") - .position(100.0, 0.0) - .input() - .build() - .unwrap(); - let n1_node = base.get_node(&n1).expect("source node exists"); - let n2_node = base.get_node(&n2).expect("target node exists"); - let edge = base - .new_edge() - .source(n1_node.outputs()[0]) - .target(n2_node.inputs()[0]); - - assert_command_interop( - &base, - || Box::new(CreateEdge::new(edge.clone())), - "CreateEdge", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/port/interaction.rs b/crates/ferrum-flow/src/plugins/port/interaction.rs deleted file mode 100644 index 880eb3659e..0000000000 --- a/crates/ferrum-flow/src/plugins/port/interaction.rs +++ /dev/null @@ -1,458 +0,0 @@ -use std::{collections::HashSet, sync::Arc}; - -use gpui::{Bounds, Element, MouseButton, Pixels, Point, Styled as _, canvas, px, rgb}; - -use crate::{ - DefaultEdgeValidator, EdgeValidator, Graph, PortId, PortKind, PortPosition, ToastMessage, - canvas::Interaction, - plugin::{FlowEvent, InputEvent, Plugin, RenderContext}, - plugins::port::{edge_bezier, filled_disc_path, port_screen_big_bounds, port_screen_bounds}, -}; - -use super::command::CreateEdge; - -/// Dangling link from a port to a world-space endpoint (shown with a dot until the user clicks it). -#[derive(Clone, Copy)] -struct PendingPortLink { - source_port: PortId, - end_world: Point, -} - -/// Internal: interaction finished on empty canvas — queue for [`PortInteractionPlugin`]. -#[derive(Clone, Copy)] -struct PendingLinkCommitted { - source_port: PortId, - end_world: Point, -} - -pub struct PortInteractionPlugin { - pending: Option, - validator: Arc, -} - -impl Default for PortInteractionPlugin { - fn default() -> Self { - Self::new() - } -} - -impl PortInteractionPlugin { - pub fn new() -> Self { - Self { - pending: None, - validator: Arc::new(DefaultEdgeValidator), - } - } - - pub fn validator(mut self, validator: impl EdgeValidator + 'static) -> Self { - self.validator = Arc::new(validator); - self - } - - fn facing_position(p: PortPosition) -> PortPosition { - match p { - PortPosition::Left => PortPosition::Right, - PortPosition::Right => PortPosition::Left, - PortPosition::Top => PortPosition::Bottom, - PortPosition::Bottom => PortPosition::Top, - } - } - - fn pending_dot_contains_screen( - ctx: &crate::plugin::PluginContext, - end_world: Point, - screen: Point, - ) -> bool { - let c = ctx.world_to_screen(end_world); - let dx: f32 = (screen.x - c.x).into(); - let dy: f32 = (screen.y - c.y).into(); - let rf: f32 = px(10.0).into(); - dx * dx + dy * dy <= rf * rf - } - - fn finish_pending_link(&mut self, ctx: &mut crate::plugin::PluginContext, p: PendingPortLink) { - let Some(source) = ctx.graph.get_port(&p.source_port).cloned() else { - return; - }; - - let mut builder = ctx.create_node(""); - builder = match source.kind() { - PortKind::Output => builder.input(), - PortKind::Input => builder.output(), - }; - - let (mut new_node, new_ports, _) = builder.build_raw(); - - let Some(connect_port) = (match source.kind() { - PortKind::Output => new_ports.iter().find(|p| p.kind() == PortKind::Input), - PortKind::Input => new_ports.iter().find(|p| p.kind() == PortKind::Output), - }) else { - return; - }; - - let mut scratch = Graph::new(); - scratch.add_node(new_node.clone()); - for port in &new_ports { - scratch.add_port(port.clone()); - } - let offset = ctx.port_world_offset_relative(&scratch, &new_node, connect_port); - new_node.set_position_with_point(Point::new( - p.end_world.x - offset.x, - p.end_world.y - offset.y, - )); - - let edge = match source.kind() { - PortKind::Output => { - let Some(in_port) = new_node.inputs().first().copied() else { - return; - }; - ctx.new_edge().source(p.source_port).target(in_port) - } - PortKind::Input => { - let Some(out_port) = new_node.outputs().first().copied() else { - return; - }; - ctx.new_edge().source(out_port).target(p.source_port) - } - }; - - ctx.execute_command(super::command::CreateNode::new(new_node)); - for port in new_ports { - ctx.execute_command(super::command::CreatePort::new(port)); - } - - ctx.execute_command(CreateEdge::new(edge)); - } - - #[allow(clippy::too_many_arguments)] - fn paint_wire_and_dot( - win: &mut gpui::Window, - origin: Point, - start: Point, - end: Point, - start_position: PortPosition, - target_position: PortPosition, - viewport: &crate::Viewport, - line_rgb: u32, - dot_rgb: u32, - ) { - if let Ok(path) = edge_bezier( - start + origin, - start_position, - target_position, - end + origin, - viewport, - ) { - win.paint_path(path, rgb(line_rgb)); - } - if let Ok(dot) = filled_disc_path(end + origin, px(6.0)) { - win.paint_path(dot, rgb(dot_rgb)); - } - } -} - -impl Plugin for PortInteractionPlugin { - fn name(&self) -> &'static str { - "port_interaction" - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::plugin::EventResult { - if let Some(p) = event.as_custom::() { - self.pending = Some(PendingPortLink { - source_port: p.source_port, - end_world: p.end_world, - }); - return crate::plugin::EventResult::Stop; - } - - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event { - if ev.button != MouseButton::Left { - return crate::plugin::EventResult::Continue; - } - if let Some(pend) = self.pending - && Self::pending_dot_contains_screen(ctx, pend.end_world, ev.position) - { - self.pending = None; - self.finish_pending_link(ctx, pend); - return crate::plugin::EventResult::Stop; - } - - let visible_nodes: HashSet<_> = ctx - .graph - .nodes() - .iter() - .filter(|(_, node)| ctx.is_node_visible_node(node)) - .map(|(id, _)| *id) - .collect(); - let candidate_ports: Vec = ctx - .graph - .ports() - .iter() - .filter(|(_, port)| visible_nodes.contains(&port.node_id())) - .map(|(_, port)| (port.id(), port.position())) - .filter_map(|(id, position)| { - let bounds = port_screen_bounds(id, ctx)?; - let big_bounds = port_screen_big_bounds(id, ctx)?; - Some(PortHitCandidate { - id, - position, - bounds, - big_bounds, - }) - }) - .collect(); - - let mouse_world = ctx.screen_to_world(ev.position); - let port_hit = candidate_ports - .iter() - .find(|c| c.bounds.contains(&mouse_world)) - .map(|c| (c.id, c.position)); - - if let Some((port_id, position)) = port_hit { - self.pending = None; - ctx.start_interaction(PortConnecting { - port_id, - position, - target_position: PortPosition::Left, - candidate_ports, - mouse: Some(ev.position), - validator: self.validator.clone(), - validation_error: None, - hovered_port: None, - }); - return crate::plugin::EventResult::Stop; - } - - if self.pending.take().is_some() { - ctx.notify(); - } - } - - crate::plugin::EventResult::Continue - } - - fn priority(&self) -> i32 { - 125 - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let p = self.pending.as_ref()?; - let start = ctx.port_screen_center_by_port_id(p.source_port)?; - let end = ctx.world_to_screen(p.end_world); - let source_port = ctx.graph.get_port(&p.source_port)?; - let start_position = source_port.position(); - let target_position = Self::facing_position(start_position); - let viewport = ctx.viewport().clone(); - let line_rgb = ctx.theme.port_preview_line; - let dot_rgb = ctx.theme.port_preview_dot; - - Some( - canvas( - move |_, _, _| (start_position, target_position, viewport, line_rgb, dot_rgb), - move |bounds, (sp, tp, vp, lr, dr), win, _| { - Self::paint_wire_and_dot(win, bounds.origin, start, end, sp, tp, &vp, lr, dr); - }, - ) - .absolute() - .size_full() - .into_any(), - ) - } - - fn render_layer(&self) -> crate::plugin::RenderLayer { - crate::plugin::RenderLayer::Interaction - } -} - -struct PortConnecting { - port_id: PortId, - position: PortPosition, - target_position: PortPosition, - /// Visible port candidates captured when the interaction starts with precomputed hit bounds. - candidate_ports: Vec, - /// Cursor in **screen** space (matches port screen center / bezier end). - mouse: Option>, - validator: Arc, - /// Validation state for current drag target. `Some(Err)` means invalid link preview. - validation_error: Option<()>, - /// Candidate port currently hovered by cursor (if any). - hovered_port: Option, -} - -#[derive(Clone, Copy)] -struct PortHitCandidate { - id: PortId, - position: PortPosition, - bounds: Bounds, - big_bounds: Bounds, -} - -impl Interaction for PortConnecting { - fn on_mouse_move( - &mut self, - event: &gpui::MouseMoveEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::canvas::InteractionResult { - self.mouse = Some(event.position); - let mouse_world = ctx.screen_to_world(event.position); - self.validation_error = None; - self.hovered_port = None; - if let Some(candidate) = self - .candidate_ports - .iter() - .find(|c| c.big_bounds.contains(&mouse_world)) - { - let port_id = candidate.id; - if port_id != self.port_id { - self.target_position = candidate.position; - self.hovered_port = Some(port_id); - - let Some(source_port) = ctx.graph.get_port(&self.port_id) else { - ctx.notify(); - return crate::canvas::InteractionResult::Continue; - }; - let Some(target_port) = ctx.graph.get_port(&port_id) else { - ctx.notify(); - return crate::canvas::InteractionResult::Continue; - }; - - let (source_port, target_port) = match (source_port.kind(), target_port.kind()) { - (PortKind::Input, PortKind::Output) => (target_port, source_port), - _ => (source_port, target_port), - }; - - if self - .validator - .validate(source_port, target_port, ctx) - .is_err() - { - self.validation_error = Some(()); - } - } - } - ctx.notify(); - crate::canvas::InteractionResult::Continue - } - - fn on_mouse_up( - &mut self, - ev: &gpui::MouseUpEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::canvas::InteractionResult { - let mouse_world = ctx.screen_to_world(ev.position); - if let Some(candidate) = self - .candidate_ports - .iter() - .find(|c| c.bounds.contains(&mouse_world)) - { - let port_id = candidate.id; - let Some(target_port) = ctx.graph.get_port(&port_id) else { - return crate::canvas::InteractionResult::End; - }; - let Some(source_port) = ctx.graph.get_port(&self.port_id) else { - return crate::canvas::InteractionResult::End; - }; - - let (source_port, target_port) = match (source_port.kind(), target_port.kind()) { - (PortKind::Input, PortKind::Output) => (target_port, source_port), - _ => (source_port, target_port), - }; - - match self.validator.validate(source_port, target_port, ctx) { - Ok(_) => { - let edge = ctx - .new_edge() - .source(source_port.id()) - .target(target_port.id()); - ctx.execute_command(CreateEdge::new(edge)); - } - Err(err) => { - ctx.emit(FlowEvent::custom(ToastMessage::error( - err.message().to_string(), - ))); - } - } - - return crate::canvas::InteractionResult::End; - } - - ctx.emit(FlowEvent::custom(PendingLinkCommitted { - source_port: self.port_id, - end_world: mouse_world, - })); - crate::canvas::InteractionResult::End - } - - fn render(&self, ctx: &mut RenderContext) -> Option { - let mouse = self.mouse?; - let start = ctx.port_screen_center_by_port_id(self.port_id)?; - let position = self.position; - let target_position = self.target_position; - let viewport = ctx.viewport().clone(); - let has_validation_error = self.validation_error.is_some(); - let line_rgb = if has_validation_error { - ctx.theme.error - } else { - ctx.theme.port_preview_line - }; - let dot_rgb = if has_validation_error { - ctx.theme.error - } else { - ctx.theme.port_preview_dot - }; - let target_highlight = if has_validation_error { - self.hovered_port.and_then(|port_id| { - let port = ctx.graph.get_port(&port_id)?; - let center = ctx.port_screen_center_by_port_id(port_id)?; - let size = *port.size_ref(); - let width: f32 = (size.width * ctx.viewport().zoom()).into(); - let height: f32 = (size.height * ctx.viewport().zoom()).into(); - let radius = px(width.min(height) / 2.0); - Some((center, radius)) - }) - } else { - None - }; - - Some( - canvas( - move |_, _, _| { - ( - position, - target_position, - viewport, - line_rgb, - dot_rgb, - target_highlight, - ) - }, - move |bounds, (position, target_position, viewport, lr, dr, th), win, _| { - let origin = bounds.origin; - PortInteractionPlugin::paint_wire_and_dot( - win, - origin, - start, - mouse, - position, - target_position, - &viewport, - lr, - dr, - ); - if let Some((center, radius)) = th - && let Ok(dot) = filled_disc_path(center + origin, radius) - { - win.paint_path(dot, rgb(lr)); - } - }, - ) - .absolute() - .size_full() - .into_any(), - ) - } -} diff --git a/crates/ferrum-flow/src/plugins/port/mod.rs b/crates/ferrum-flow/src/plugins/port/mod.rs deleted file mode 100644 index 193403abad..0000000000 --- a/crates/ferrum-flow/src/plugins/port/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod interaction; -mod utils; - -pub use interaction::PortInteractionPlugin; - -mod command; -mod validator; - -pub use command::{CreateEdge, CreateNode, CreatePort}; -pub use validator::{ - DefaultEdgeValidator, EdgeValidationError, EdgeValidationErrorCode, EdgeValidator, -}; - -#[allow(deprecated, unused_imports)] -pub use utils::port_screen_position; -pub use utils::{edge_bezier, filled_disc_path, port_screen_big_bounds, port_screen_bounds}; diff --git a/crates/ferrum-flow/src/plugins/port/utils.rs b/crates/ferrum-flow/src/plugins/port/utils.rs deleted file mode 100644 index 973a8f787b..0000000000 --- a/crates/ferrum-flow/src/plugins/port/utils.rs +++ /dev/null @@ -1,81 +0,0 @@ -use gpui::{Bounds, Path, PathBuilder, Pixels, Point, Size, px}; - -use crate::{PortId, PortPosition, RenderContext, Viewport}; - -#[deprecated(note = "use `ctx.port_screen_center_by_port_id(port_id)`")] -#[allow(dead_code)] // kept for re-export; callers should migrate to `RenderContext` methods -pub fn port_screen_position(port_id: PortId, ctx: &RenderContext) -> Option> { - ctx.port_screen_center_by_port_id(port_id) -} - -pub fn port_screen_bounds( - port_id: PortId, - ctx: &crate::plugin::PluginContext, -) -> Option> { - let port = &ctx.graph.get_port(&port_id)?; - let node = &ctx.nodes().get(&port.node_id())?; - - let node_pos = node.point(); - - let offset = ctx.port_offset_cached(&port.node_id(), &port_id)?; - let size = *port.size_ref(); - - Some(Bounds::new( - node_pos + offset - Point::new(size.width / 2.0, size.height / 2.0), - size, - )) -} - -pub fn port_screen_big_bounds( - port_id: PortId, - ctx: &crate::plugin::PluginContext, -) -> Option> { - let mut bounds = port_screen_bounds(port_id, ctx)?; - - let offset_width = px(15.0) - bounds.size.width / 2.0; - let offset_height = px(15.0) - bounds.size.height / 2.0; - - bounds.origin -= Point::new(offset_width, offset_height); - - bounds.size = Size { - width: px(30.0), - height: px(30.0), - }; - - Some(bounds) -} - -/// Filled circle in screen space (for dangling-connection endpoint marker). -pub fn filled_disc_path( - center: Point, - radius: Pixels, -) -> Result, anyhow::Error> { - let r: f32 = radius.into(); - let cx: f32 = center.x.into(); - let cy: f32 = center.y.into(); - const SEGMENTS: usize = 28; - let mut pts: Vec> = Vec::with_capacity(SEGMENTS); - for i in 0..SEGMENTS { - let t = i as f32 / SEGMENTS as f32 * std::f32::consts::TAU; - pts.push(Point::new(px(cx + r * t.cos()), px(cy + r * t.sin()))); - } - let mut pb = PathBuilder::fill(); - pb.add_polygon(&pts, true); - pb.build() -} - -pub fn edge_bezier( - start: Point, - start_position: PortPosition, - end_poisition: PortPosition, - end: Point, - viewport: &Viewport, -) -> Result, anyhow::Error> { - let control_a = viewport.edge_control_point(start, start_position); - let control_b = viewport.edge_control_point(end, end_poisition); - let mut line = PathBuilder::stroke(px(1.0)); - line.move_to(start); - line.cubic_bezier_to(end, control_a, control_b); - - line.build() -} diff --git a/crates/ferrum-flow/src/plugins/port/validator.rs b/crates/ferrum-flow/src/plugins/port/validator.rs deleted file mode 100644 index 75f9126463..0000000000 --- a/crates/ferrum-flow/src/plugins/port/validator.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::fmt::Display; - -use crate::{PluginContext, Port, PortKind}; - -/// Validates whether an edge may be created between two ports. -pub trait EdgeValidator: Send + Sync { - fn validate( - &self, - from: &Port, - to: &Port, - ctx: &PluginContext, - ) -> Result<(), EdgeValidationError>; -} - -#[derive(Debug, Clone)] -pub struct EdgeValidationError { - code: EdgeValidationErrorCode, - message: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EdgeValidationErrorCode { - /// The two ports are not one output and one input. - KindMismatch, - /// Both ports belong to the same node. - SameNode, - /// Port types are incompatible (reserved for stricter validators). - TypeMismatch, - /// Target input already has a connection (reserved for stricter validators). - AlreadyConnected, - /// Plugin-specific failure reason. - Custom(String), -} - -impl EdgeValidationError { - pub fn new(code: EdgeValidationErrorCode, message: String) -> Self { - Self { code, message } - } - - pub fn kind_mismatch(message: String) -> Self { - Self::new(EdgeValidationErrorCode::KindMismatch, message) - } - - pub fn same_node(message: String) -> Self { - Self::new(EdgeValidationErrorCode::SameNode, message) - } - - pub fn type_mismatch(message: String) -> Self { - Self::new(EdgeValidationErrorCode::TypeMismatch, message) - } - - pub fn already_connected(message: String) -> Self { - Self::new(EdgeValidationErrorCode::AlreadyConnected, message) - } - - pub fn custom(ty: String, message: String) -> Self { - Self::new(EdgeValidationErrorCode::Custom(ty), message) - } - - pub fn code(&self) -> &EdgeValidationErrorCode { - &self.code - } - - pub fn message(&self) -> &str { - &self.message - } -} - -impl Display for EdgeValidationErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EdgeValidationErrorCode::KindMismatch => write!(f, "KindMismatch"), - EdgeValidationErrorCode::SameNode => write!(f, "SameNode"), - EdgeValidationErrorCode::TypeMismatch => write!(f, "TypeMismatch"), - EdgeValidationErrorCode::AlreadyConnected => write!(f, "AlreadyConnected"), - EdgeValidationErrorCode::Custom(ty) => write!(f, "Custom({})", ty), - } - } -} - -/// Permissive default: requires one output and one input on different nodes; ignores -/// `port_type` and does not check for duplicate edges. -#[derive(Debug, Default, Clone, Copy)] -pub struct DefaultEdgeValidator; - -impl EdgeValidator for DefaultEdgeValidator { - fn validate( - &self, - from: &Port, - to: &Port, - _ctx: &PluginContext, - ) -> Result<(), EdgeValidationError> { - if from.node_id() == to.node_id() { - return Err(EdgeValidationError::same_node( - "Cannot connect two ports on the same node.".into(), - )); - } - - let one_output_one_input = matches!( - (from.kind(), to.kind()), - (PortKind::Output, PortKind::Input) | (PortKind::Input, PortKind::Output) - ); - if !one_output_one_input { - return Err(EdgeValidationError::kind_mismatch( - "A connection must be between an output port and an input port.".into(), - )); - } - - Ok(()) - } -} diff --git a/crates/ferrum-flow/src/plugins/select_all_viewport.rs b/crates/ferrum-flow/src/plugins/select_all_viewport.rs deleted file mode 100644 index 3e8dbab2a2..0000000000 --- a/crates/ferrum-flow/src/plugins/select_all_viewport.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::collections::HashSet; - -use crate::plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier}; - -/// Select every node and edge that intersects the current window viewport (⌘A / Ctrl+A). -pub struct SelectAllViewportPlugin; - -impl SelectAllViewportPlugin { - pub fn new() -> Self { - Self - } -} - -impl Default for SelectAllViewportPlugin { - fn default() -> Self { - Self::new() - } -} - -fn select_visible(ctx: &mut PluginContext) { - let visible_nodes: HashSet<_> = ctx - .graph - .node_order() - .iter() - .filter(|id| ctx.is_node_visible(id)) - .copied() - .collect(); - - let visible_edges: HashSet<_> = ctx - .graph - .edges_values() - .filter(|e| ctx.is_edge_visible(e)) - .map(|e| e.id) - .collect(); - - ctx.graph.set_selected_node(visible_nodes); - ctx.graph.set_selected_edge(visible_edges); -} - -pub(crate) fn select_all_in_viewport(ctx: &mut PluginContext) { - select_visible(ctx); -} - -impl Plugin for SelectAllViewportPlugin { - fn name(&self) -> &'static str { - "select_all_viewport" - } - - fn priority(&self) -> i32 { - 93 - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event - && primary_platform_modifier(ev) - && ev.keystroke.key == "a" - { - select_visible(ctx); - ctx.notify(); - return crate::plugin::EventResult::Stop; - } - crate::plugin::EventResult::Continue - } -} diff --git a/crates/ferrum-flow/src/plugins/selection/mod.rs b/crates/ferrum-flow/src/plugins/selection/mod.rs deleted file mode 100644 index b21ea004fb..0000000000 --- a/crates/ferrum-flow/src/plugins/selection/mod.rs +++ /dev/null @@ -1,340 +0,0 @@ -use std::collections::HashMap; -use std::time::{Duration, Instant}; - -use gpui::{ - AnyElement, Bounds, Element, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, Point, Size, - Styled, div, px, rgb, rgba, -}; - -use crate::{ - FlowTheme, NodeId, - canvas::{Interaction, InteractionResult}, - plugin::{ - EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer, - }, -}; - -const DRAG_THRESHOLD: Pixels = px(2.0); -const DRAG_COMMAND_INTERVAL: Duration = Duration::from_millis(50); - -pub struct SelectionPlugin { - selected: Option, -} - -struct Selected { - bounds: Bounds, - nodes: HashMap>, -} - -impl SelectionPlugin { - pub fn new() -> Self { - Self { selected: None } - } -} - -impl Default for SelectionPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for SelectionPlugin { - fn name(&self) -> &'static str { - "selection" - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event { - if ev.button != MouseButton::Left { - return EventResult::Continue; - } - if !ev.modifiers.shift { - let start = ctx.screen_to_world(ev.position); - if let Some(Selected { bounds, nodes }) = self.selected.take() - && bounds.contains(&start) - { - ctx.start_interaction(SelectionInteraction::start_move(start, bounds, nodes)); - - return EventResult::Stop; - } - - ctx.start_interaction(SelectionInteraction::new(start)); - return EventResult::Stop; - } - } else if let Some(SelectedEvent { bounds, nodes }) = event.as_custom() { - self.selected = if nodes.is_empty() { - None - } else { - Some(Selected { - bounds: *bounds, - nodes: nodes.clone(), - }) - }; - return EventResult::Stop; - } else if let FlowEvent::Input(InputEvent::Hover(false)) = event { - self.selected = None; - } - EventResult::Continue - } - - fn priority(&self) -> i32 { - 100 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Selection - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - self.selected.as_ref().map(|Selected { bounds, .. }| { - let top_left = ctx.world_to_screen(bounds.origin); - - let size = Size::new( - ctx.world_length_to_screen(bounds.size.width), - ctx.world_length_to_screen(bounds.size.height), - ); - render_rect(Bounds::new(top_left, size), ctx.theme) - }) - } -} - -pub struct SelectionInteraction { - state: SelectionState, - last_drag_command_at: Option, -} - -enum SelectionState { - Pending { - start: Point, - }, - Selecting { - start: Point, - end: Point, - }, - Moving { - start_mouse: Point, - start_bounds: Bounds, - bounds: Bounds, - nodes: HashMap>, - }, -} -struct SelectedEvent { - bounds: Bounds, - nodes: HashMap>, -} - -impl SelectionInteraction { - pub fn new(start: Point) -> Self { - Self { - state: SelectionState::Pending { start }, - last_drag_command_at: None, - } - } - pub fn start_move( - mouse: Point, - bounds: Bounds, - nodes: HashMap>, - ) -> Self { - Self { - state: SelectionState::Moving { - start_mouse: mouse, - start_bounds: bounds, - bounds, - nodes, - }, - last_drag_command_at: None, - } - } -} - -impl Interaction for SelectionInteraction { - fn on_mouse_move(&mut self, ev: &MouseMoveEvent, ctx: &mut PluginContext) -> InteractionResult { - let mouse_world = ctx.screen_to_world(ev.position); - match &mut self.state { - SelectionState::Pending { start } => { - let delta = mouse_world - *start; - - if delta.x.abs() > DRAG_THRESHOLD && delta.y.abs() > DRAG_THRESHOLD { - self.state = SelectionState::Selecting { - start: *start, - end: mouse_world, - }; - - ctx.notify(); - } - } - - SelectionState::Selecting { end, .. } => { - *end = mouse_world; - ctx.notify(); - } - - SelectionState::Moving { - start_mouse, - start_bounds, - bounds, - nodes, - } => { - let delta = mouse_world - *start_mouse; - - for (id, start_pos) in nodes.iter() { - if let Some(node) = ctx.get_node_mut(id) { - node.set_position(start_pos.x + delta.x, start_pos.y + delta.y); - } - } - *bounds = Bounds::new(start_bounds.origin + delta, start_bounds.size); - - if ctx.has_sync_plugin() { - let now = Instant::now(); - let should_command = self - .last_drag_command_at - .map(|t| now.duration_since(t) >= DRAG_COMMAND_INTERVAL) - .unwrap_or(true); - if should_command { - let start_position: Vec<_> = - nodes.iter().map(|(id, point)| (*id, *point)).collect(); - ctx.execute_command(super::node::DragNodesCommand::new( - &start_position, - ctx, - )); - self.last_drag_command_at = Some(now); - } - } - - ctx.notify(); - } - } - - InteractionResult::Continue - } - fn on_mouse_up(&mut self, _ev: &MouseUpEvent, ctx: &mut PluginContext) -> InteractionResult { - match &mut self.state { - SelectionState::Pending { .. } => InteractionResult::End, - - SelectionState::Selecting { start, end } => { - let rect = normalize_rect(*start, *end); - - ctx.clear_selected_node(); - - let mut nodes: HashMap> = HashMap::new(); - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - - for node in ctx - .graph - .nodes() - .values() - .filter(|node| ctx.is_node_visible_node(node)) - .filter(|node| rect.intersects(&node.bounds())) - { - let (x, y) = node.position(); - let size = *node.size_ref(); - nodes.insert(node.id(), node.point()); - min_x = min_x.min(x.into()); - min_y = min_y.min(y.into()); - max_x = max_x.max((x + size.width).into()); - max_y = max_y.max((y + size.height).into()); - } - - for id in nodes.keys().copied() { - ctx.add_selected_node(id, true); - } - - let bounds = if nodes.is_empty() { - rect - } else { - Bounds::new( - Point::new(px(min_x), px(min_y)), - Size::new(px(max_x - min_x), px(max_y - min_y)), - ) - }; - - ctx.cancel_interaction(); - ctx.emit(FlowEvent::custom(SelectedEvent { bounds, nodes })); - - InteractionResult::End - } - - SelectionState::Moving { bounds, nodes, .. } => { - let bounds = *bounds; - - let mut new_nodes = HashMap::new(); - for (id, _) in nodes.iter() { - ctx.add_selected_node(*id, true); - if let Some(node) = ctx.get_node(id) { - new_nodes.insert(*id, node.point()); - } - } - - let start_position: Vec<_> = - nodes.iter().map(|(id, point)| (*id, *point)).collect(); - - ctx.execute_command(super::node::DragNodesCommand::new(&start_position, ctx)); - - ctx.emit(FlowEvent::custom(SelectedEvent { - bounds, - nodes: new_nodes, - })); - - InteractionResult::End - } - } - } - fn render(&self, ctx: &mut RenderContext) -> Option { - match &self.state { - SelectionState::Selecting { start, end } => { - let rect = normalize_rect(*start, *end); - - let top_left = ctx.world_to_screen(rect.origin); - - let size = Size::new( - ctx.world_length_to_screen(rect.size.width), - ctx.world_length_to_screen(rect.size.height), - ); - - Some(render_rect(Bounds::new(top_left, size), ctx.theme)) - } - - SelectionState::Moving { bounds, .. } => { - let top_left = ctx.world_to_screen(bounds.origin); - - let size = Size::new( - ctx.world_length_to_screen(bounds.size.width), - ctx.world_length_to_screen(bounds.size.height), - ); - Some(render_rect(Bounds::new(top_left, size), ctx.theme)) - } - - _ => None, - } - } -} - -fn normalize_rect(start: Point, end: Point) -> Bounds { - let x = start.x.min(end.x); - let y = start.y.min(end.y); - - let w = (end.x - start.x).abs(); - let h = (end.y - start.y).abs(); - - Bounds::new(Point::new(x, y), Size::new(w, h)) -} - -fn render_rect(bounds: Bounds, theme: &FlowTheme) -> AnyElement { - div() - .absolute() - .left(bounds.origin.x) - .top(bounds.origin.y) - .w(bounds.size.width) - .h(bounds.size.height) - .border(px(1.0)) - .border_color(rgb(theme.selection_rect_border)) - .bg(rgba(theme.selection_rect_fill_rgba)) - .into_any() -} diff --git a/crates/ferrum-flow/src/plugins/snap_guides.rs b/crates/ferrum-flow/src/plugins/snap_guides.rs deleted file mode 100644 index 9630511f58..0000000000 --- a/crates/ferrum-flow/src/plugins/snap_guides.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Alignment guides while dragging nodes. -//! -//! Subscribes to [`NodeDragEvent`](crate::plugins::node::NodeDragEvent) from -//! [`crate::plugins::NodeInteractionPlugin`] and runs -//! [`compute_alignment_guides`] only here. -//! This keeps [`crate::canvas::InteractionState`] free of overlay-specific fields. - -use std::collections::HashSet; - -use gpui::{AnyElement, Div, Element, ParentElement, Pixels, Point, Styled, div, px, rgb}; - -use crate::{ - Graph, NodeId, - plugin::{EventResult, FlowEvent, Plugin, PluginContext, RenderContext, RenderLayer}, - plugins::node::NodeDragEvent, - theme::FlowTheme, -}; - -/// Screen-space snap distance, converted to world units via `threshold / zoom`. -const SNAP_SCREEN_PX: f32 = 4.0; - -/// World-space lines to draw as alignment guides (full width / height of the canvas view). -#[derive(Debug, Clone, Default)] -struct AlignmentGuides { - pub vertical_x: Vec, - pub horizontal_y: Vec, -} - -fn union_drag_bounds(graph: &Graph, dragged_ids: &[NodeId]) -> Option> { - let mut min_x = f32::MAX; - let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; - let mut any = false; - - for id in dragged_ids { - let Some(n) = graph.get_node(id) else { - continue; - }; - any = true; - let b = n.bounds(); - let l: f32 = b.origin.x.into(); - let t: f32 = b.origin.y.into(); - let r: f32 = (b.origin.x + b.size.width).into(); - let bot: f32 = (b.origin.y + b.size.height).into(); - min_x = min_x.min(l); - min_y = min_y.min(t); - max_x = max_x.max(r); - max_y = max_y.max(bot); - } - - if !any { - return None; - } - - Some(gpui::Bounds::new( - Point::new(min_x.into(), min_y.into()), - gpui::Size::new((max_x - min_x).into(), (max_y - min_y).into()), - )) -} - -fn dedup_sorted_coords(mut v: Vec) -> Vec { - v.sort_by(|a, b| f32::total_cmp(&(*a).into(), &(*b).into())); - v.dedup_by(|a, b| { - let af: f32 = (*a).into(); - let bf: f32 = (*b).into(); - af == bf - }); - v -} - -/// Computes alignment guides for the current drag. Skips nodes that are not on-screen and uses an -/// AABB broadphase so distant nodes are not considered. -fn compute_alignment_guides( - ctx: &PluginContext, - dragged_ids: &[NodeId], -) -> Option { - let dragged_set: HashSet<&NodeId> = dragged_ids.iter().collect(); - let thr = ctx.screen_length_to_world(px(SNAP_SCREEN_PX)); - let union = union_drag_bounds(ctx.graph, dragged_ids)?; - let dl = union.origin.x; - let dr = union.origin.x + union.size.width; - let dcx = (dl + dr) * 0.5; - let dt = union.origin.y; - let db = union.origin.y + union.size.height; - let dcy = (dt + db) * 0.5; - - let drag_x_lo = dl - thr; - let drag_x_hi = dr + thr; - let drag_y_lo = dt - thr; - let drag_y_hi = db + thr; - - let drag_xs = [dl, dcx, dr]; - let drag_ys = [dt, dcy, db]; - - let mut ref_x: Vec = Vec::new(); - let mut ref_y: Vec = Vec::new(); - - for (id, node) in ctx.graph.nodes() { - if dragged_set.contains(id) { - continue; - } - if !ctx.is_node_visible_node(node) { - continue; - } - - let b = node.bounds(); - let rl = b.origin.x; - let rr = rl + b.size.width; - let rcx = (rl + rr) * 0.5; - let rt = b.origin.y; - let rb = rt + b.size.height; - let rcy = (rt + rb) * 0.5; - - let can_vertical = !(rr < drag_x_lo || rl > drag_x_hi); - let can_horizontal = !(rb < drag_y_lo || rt > drag_y_hi); - if !can_vertical && !can_horizontal { - continue; - } - - if can_vertical { - ref_x.extend([rl, rcx, rr]); - } - if can_horizontal { - ref_y.extend([rt, rcy, rb]); - } - } - - let mut vertical_x = Vec::new(); - for rx in ref_x { - if drag_xs.iter().any(|dx| (*dx - rx).abs() <= thr) { - vertical_x.push(rx); - } - } - - let mut horizontal_y = Vec::new(); - for ry in ref_y { - if drag_ys.iter().any(|dy| (*dy - ry).abs() <= thr) { - horizontal_y.push(ry); - } - } - - vertical_x = dedup_sorted_coords(vertical_x); - horizontal_y = dedup_sorted_coords(horizontal_y); - - if vertical_x.is_empty() && horizontal_y.is_empty() { - return None; - } - - Some(AlignmentGuides { - vertical_x, - horizontal_y, - }) -} - -pub struct SnapGuidesPlugin { - guides: Option, -} - -impl SnapGuidesPlugin { - pub fn new() -> Self { - Self { guides: None } - } -} - -impl Default for SnapGuidesPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for SnapGuidesPlugin { - fn name(&self) -> &'static str { - "snap_guides" - } - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let Some(evt) = event.as_custom::() { - match evt { - NodeDragEvent::Tick(ids) => { - self.guides = compute_alignment_guides(ctx, ids.as_ref()); - ctx.notify(); - } - NodeDragEvent::End => { - self.guides = None; - ctx.notify(); - } - } - } - EventResult::Continue - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let guides = self.guides.as_ref()?; - let wb = ctx.window_bounds()?; - let w = wb.size.width; - let h = wb.size.height; - let theme = ctx.theme; - - let vx = guides.vertical_x.iter().map(|wx| vline(*wx, h, ctx, theme)); - let hy = guides - .horizontal_y - .iter() - .map(|wy| hline(*wy, w, ctx, theme)); - - Some( - div() - .absolute() - .size_full() - .children(vx.chain(hy)) - .into_any(), - ) - } - - fn priority(&self) -> i32 { - 118 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Interaction - } -} - -fn vline(wx: Pixels, win_h: Pixels, ctx: &RenderContext<'_>, theme: &FlowTheme) -> Div { - let sx = ctx.world_to_screen(Point::new(wx, px(0.0))).x; - div() - .absolute() - .left(sx) - .top(px(0.0)) - .w(px(1.0)) - .h(win_h) - .bg(rgb(theme.selection_rect_border)) -} - -fn hline(wy: Pixels, win_w: Pixels, ctx: &RenderContext<'_>, theme: &FlowTheme) -> Div { - let sy = ctx.world_to_screen(Point::new(px(0.0), wy)).y; - div() - .absolute() - .left(px(0.0)) - .top(sy) - .w(win_w) - .h(px(1.0)) - .bg(rgb(theme.selection_rect_border)) -} diff --git a/crates/ferrum-flow/src/plugins/toast.rs b/crates/ferrum-flow/src/plugins/toast.rs deleted file mode 100644 index 71fc983065..0000000000 --- a/crates/ferrum-flow/src/plugins/toast.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::{ - collections::VecDeque, - time::{Duration, Instant}, -}; - -use gpui::{Element as _, ParentElement as _, Styled as _, div, px, rgb}; - -use crate::{ - FlowTheme, - plugin::{FlowEvent, Plugin, PluginContext, RenderContext}, -}; - -const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(3000); -const MAX_TOASTS: usize = 4; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ToastLevel { - Info, - Success, - Warning, - Error, -} - -#[derive(Debug, Clone)] -pub struct ToastMessage { - text: String, - level: ToastLevel, - duration: Duration, -} - -impl ToastMessage { - pub fn new(text: impl Into, level: ToastLevel) -> Self { - Self { - text: text.into(), - level, - duration: DEFAULT_TOAST_DURATION, - } - } - - pub fn info(text: impl Into) -> Self { - Self::new(text, ToastLevel::Info) - } - - pub fn success(text: impl Into) -> Self { - Self::new(text, ToastLevel::Success) - } - - pub fn warning(text: impl Into) -> Self { - Self::new(text, ToastLevel::Warning) - } - - pub fn error(text: impl Into) -> Self { - Self::new(text, ToastLevel::Error) - } - - pub fn with_duration(mut self, duration: Duration) -> Self { - self.duration = duration; - self - } -} - -#[derive(Debug, Clone)] -struct ToastItem { - text: String, - level: ToastLevel, - expires_at: Instant, -} - -pub struct ToastPlugin { - queue: VecDeque, -} - -impl Default for ToastPlugin { - fn default() -> Self { - Self::new() - } -} - -impl ToastPlugin { - pub fn new() -> Self { - Self { - queue: VecDeque::new(), - } - } - - fn gc_expired(&mut self) { - let now = Instant::now(); - self.queue.retain(|item| item.expires_at > now); - } - - fn push(&mut self, msg: ToastMessage) { - self.gc_expired(); - self.queue.push_back(ToastItem { - text: msg.text, - level: msg.level, - expires_at: Instant::now() + msg.duration, - }); - while self.queue.len() > MAX_TOASTS { - let _ = self.queue.pop_front(); - } - } - - fn bg_color(level: ToastLevel, theme: &FlowTheme) -> u32 { - match level { - ToastLevel::Info => theme.info, - ToastLevel::Success => theme.success, - ToastLevel::Warning => theme.warning, - ToastLevel::Error => theme.error, - } - } -} - -impl Plugin for ToastPlugin { - fn name(&self) -> &'static str { - "toast" - } - - fn on_event( - &mut self, - event: &FlowEvent, - ctx: &mut PluginContext, - ) -> crate::plugin::EventResult { - self.gc_expired(); - if let Some(msg) = event.as_custom::() { - let duration = msg.duration; - self.push(msg.clone()); - ctx.schedule_after(duration); - ctx.notify(); - } - crate::plugin::EventResult::Continue - } - - fn priority(&self) -> i32 { - 10 - } - - fn render_layer(&self) -> crate::plugin::RenderLayer { - crate::plugin::RenderLayer::Overlay - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - self.gc_expired(); - if self.queue.is_empty() { - return None; - } - - let items = self.queue.iter().rev().map(|item| { - div() - .mb_2() - .max_w(px(360.0)) - .rounded(px(8.0)) - .bg(rgb(Self::bg_color(item.level, ctx.theme))) - .px_3() - .py_2() - .child( - div() - .text_sm() - .text_color(rgb(0x00FFFFFF)) - .child(item.text.clone()), - ) - }); - - Some( - div() - .absolute() - .right(px(12.0)) - .bottom(px(12.0)) - .children(items) - .into_any(), - ) - } -} diff --git a/crates/ferrum-flow/src/plugins/viewport.rs b/crates/ferrum-flow/src/plugins/viewport.rs deleted file mode 100644 index 1ad4017afe..0000000000 --- a/crates/ferrum-flow/src/plugins/viewport.rs +++ /dev/null @@ -1,152 +0,0 @@ -use gpui::{MouseButton, Pixels, Point, px}; - -use crate::{ - canvas::{Command, Interaction, InteractionResult}, - plugin::{EventResult, FlowEvent, InputEvent, Plugin}, -}; - -pub struct ViewportPlugin; - -impl ViewportPlugin { - pub fn new() -> Self { - Self {} - } -} - -impl Default for ViewportPlugin { - fn default() -> Self { - Self::new() - } -} - -impl Plugin for ViewportPlugin { - fn name(&self) -> &'static str { - "viewport" - } - - fn on_event( - &mut self, - event: &crate::plugin::FlowEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event - && ((ev.button == MouseButton::Left && ev.modifiers.shift) - || ev.button == MouseButton::Middle) - { - ctx.start_interaction(Panning { - start_mouse: ev.position, - start_offset: ctx.offset(), - }); - return EventResult::Stop; - } else if let FlowEvent::Input(InputEvent::Wheel(ev)) = event { - let cursor = ev.position; - - let before = ctx.screen_to_world(cursor); - - let delta = f32::from(ev.delta.pixel_delta(px(1.0)).y); - if delta == 0.0 { - return EventResult::Continue; - } - - let zoom_delta = if delta > 0.0 { 0.9 } else { 1.1 }; - - ctx.set_zoom(ctx.zoom_scaled_by(zoom_delta).clamp(0.1, 3.0)); - - let after = ctx.world_to_screen(before); - - ctx.translate_offset(cursor.x - after.x, cursor.y - after.y); - ctx.notify(); - } - EventResult::Continue - } - - fn priority(&self) -> i32 { - 10 - } -} - -struct Panning { - start_mouse: Point, - start_offset: Point, -} - -impl Interaction for Panning { - fn on_mouse_move( - &mut self, - ev: &gpui::MouseMoveEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> InteractionResult { - let dx = ev.position.x - self.start_mouse.x; - let dy = ev.position.y - self.start_mouse.y; - - ctx.set_offset(Point::new( - self.start_offset.x + dx, - self.start_offset.y + dy, - )); - ctx.notify(); - - InteractionResult::Continue - } - - fn on_mouse_up( - &mut self, - _event: &gpui::MouseUpEvent, - ctx: &mut crate::plugin::PluginContext, - ) -> crate::canvas::InteractionResult { - ctx.execute_command(PanningCommand { - from: self.start_offset, - to: ctx.offset(), - }); - ctx.cancel_interaction(); - InteractionResult::End - } -} - -struct PanningCommand { - from: Point, - to: Point, -} - -impl Command for PanningCommand { - fn name(&self) -> &'static str { - "panning" - } - fn execute(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.set_offset(self.to); - } - fn undo(&mut self, ctx: &mut crate::canvas::CommandContext) { - ctx.set_offset(self.from); - } - - fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec { - vec![] - } -} - -#[cfg(test)] -mod command_interop_tests { - use gpui::{Point, px}; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::PanningCommand; - - #[test] - fn panning_command_interop() { - let base = Graph::new(); - let cmd = PanningCommand { - from: Point::new(px(0.0), px(0.0)), - to: Point::new(px(12.0), px(34.0)), - }; - assert_command_interop( - &base, - || { - Box::new(PanningCommand { - from: cmd.from, - to: cmd.to, - }) - }, - "PanningCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/viewport_frame.rs b/crates/ferrum-flow/src/plugins/viewport_frame.rs deleted file mode 100644 index 975eb72b49..0000000000 --- a/crates/ferrum-flow/src/plugins/viewport_frame.rs +++ /dev/null @@ -1,166 +0,0 @@ -use gpui::{Pixels, Point, px}; - -use crate::{ - InitPluginContext, - canvas::{Command, CommandContext}, - plugin::PluginContext, -}; - -/// Same zoom limits as [`crate::plugins::ViewportPlugin`] wheel zoom. -pub(crate) const ZOOM_MIN: f32 = 0.7; -pub(crate) const ZOOM_MAX: f32 = 3.0; -/// Inset from window edges (ratio per side). -pub(crate) const MARGIN_RATIO: f32 = 0.08; - -fn frame_params( - win_w: f32, - win_h: f32, - bx: f32, - by: f32, - bw: f32, - bh: f32, -) -> Option<(f32, Point)> { - if win_w <= 0.0 || win_h <= 0.0 { - return None; - } - - let bw_safe = bw.max(1.0); - let bh_safe = bh.max(1.0); - - let avail_w = win_w * (1.0 - 2.0 * MARGIN_RATIO); - let avail_h = win_h * (1.0 - 2.0 * MARGIN_RATIO); - let z = (avail_w / bw_safe) - .min(avail_h / bh_safe) - .clamp(ZOOM_MIN, ZOOM_MAX); - - let cx = bx + bw / 2.0; - let cy = by + bh / 2.0; - let center_x = win_w / 2.0; - let center_y = win_h / 2.0; - let new_offset = Point::new(px(center_x - cx * z), px(center_y - cy * z)); - - Some((z, new_offset)) -} - -pub(crate) struct ViewportFrameCommand { - pub from_zoom: f32, - pub from_offset: Point, - pub to_zoom: f32, - pub to_offset: Point, -} - -impl Command for ViewportFrameCommand { - fn name(&self) -> &'static str { - "viewport_frame" - } - - fn execute(&mut self, ctx: &mut CommandContext) { - ctx.set_zoom(self.to_zoom); - ctx.set_offset(self.to_offset); - } - - fn undo(&mut self, ctx: &mut CommandContext) { - ctx.set_zoom(self.from_zoom); - ctx.set_offset(self.from_offset); - } - - fn to_ops(&self, ctx: &mut CommandContext) -> Vec { - ctx.set_zoom(self.to_zoom); - ctx.set_offset(self.to_offset); - vec![] - } -} - -/// Pan + zoom so the given world-space axis-aligned box (position + size) fits the window. -pub(crate) fn frame_world_rect(ctx: &mut PluginContext, bx: f32, by: f32, bw: f32, bh: f32) { - let Some(wb) = ctx.window_bounds() else { - return; - }; - - let win_w: f32 = wb.size.width.into(); - let win_h: f32 = wb.size.height.into(); - let Some((z, new_offset)) = frame_params(win_w, win_h, bx, by, bw, bh) else { - return; - }; - - let from_zoom = ctx.zoom(); - let from_offset = ctx.offset(); - let zoom_changed = (from_zoom - z).abs() > 1e-4; - let ox: f32 = from_offset.x.into(); - let oy: f32 = from_offset.y.into(); - let nx: f32 = new_offset.x.into(); - let ny: f32 = new_offset.y.into(); - let offset_changed = (ox - nx).abs() > 0.5 || (oy - ny).abs() > 0.5; - if !zoom_changed && !offset_changed { - return; - } - - ctx.execute_command(ViewportFrameCommand { - from_zoom, - from_offset, - to_zoom: z, - to_offset: new_offset, - }); -} - -/// Same geometry as [`frame_world_rect`], but writes [`Viewport`] directly (no undo stack). -/// For [`crate::plugin::InitPluginContext`] / plugin [`Plugin::setup`]. -pub(crate) fn apply_frame_world_rect_direct( - ctx: &mut InitPluginContext, - win_w: f32, - win_h: f32, - bx: f32, - by: f32, - bw: f32, - bh: f32, -) { - let Some((z, new_offset)) = frame_params(win_w, win_h, bx, by, bw, bh) else { - return; - }; - - let zoom_changed = (ctx.zoom() - z).abs() > 1e-4; - let off = ctx.offset(); - let ox: f32 = off.x.into(); - let oy: f32 = off.y.into(); - let nx: f32 = new_offset.x.into(); - let ny: f32 = new_offset.y.into(); - let offset_changed = (ox - nx).abs() > 0.5 || (oy - ny).abs() > 0.5; - if !zoom_changed && !offset_changed { - return; - } - - ctx.set_zoom(z); - ctx.set_offset(new_offset); -} - -#[cfg(test)] -mod command_interop_tests { - use gpui::{Point, px}; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::ViewportFrameCommand; - - #[test] - fn viewport_frame_command_interop() { - let base = Graph::new(); - let cmd = ViewportFrameCommand { - from_zoom: 1.0, - from_offset: Point::new(px(0.0), px(0.0)), - to_zoom: 1.1, - to_offset: Point::new(px(8.0), px(9.0)), - }; - assert_command_interop( - &base, - || { - Box::new(ViewportFrameCommand { - from_zoom: cmd.from_zoom, - from_offset: cmd.from_offset, - to_zoom: cmd.to_zoom, - to_offset: cmd.to_offset, - }) - }, - "ViewportFrameCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/plugins/zoom_controls.rs b/crates/ferrum-flow/src/plugins/zoom_controls.rs deleted file mode 100644 index 522933be78..0000000000 --- a/crates/ferrum-flow/src/plugins/zoom_controls.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Bottom-left zoom controls (left to right: **+ − ↺ ⛶**): zoom in, zoom out, reset scale, fit entire graph. - -use gpui::{ - Bounds, IntoElement as _, MouseButton, ParentElement as _, Pixels, Point, Size, Styled as _, - div, px, rgb, -}; - -/// Unicode minus sign (not ASCII hyphen). -const LABEL_ZOOM_OUT: &str = "\u{2212}"; -const LABEL_ZOOM_IN: &str = "+"; -/// Anticlockwise open circle arrow — common “reset view” symbol. -const LABEL_RESET_ZOOM: &str = "\u{21BA}"; -/// Square four corners — “frame / fit content” (same action as [`crate::plugins::FitAllGraphPlugin`]). -const LABEL_FIT_ENTIRE_GRAPH: &str = "\u{26F6}"; - -use crate::{ - canvas::{Command, CommandContext}, - plugin::{ - EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer, - }, -}; - -use super::fit_all::fit_entire_graph; -use super::viewport_frame::{ZOOM_MAX, ZOOM_MIN}; - -const MARGIN: f32 = 16.0; -/// Square control size (width = height). -const BTN: f32 = 36.0; -const GAP: f32 = 6.0; -/// Same step as [`crate::plugins::ViewportPlugin`] wheel zoom. -const ZOOM_STEP: f32 = 1.1; - -struct ZoomControlsLayout { - zoom_in: Bounds, - zoom_out: Bounds, - reset: Bounds, - fit_entire_graph: Bounds, -} - -impl ZoomControlsLayout { - fn hit(&self, p: Point) -> Option { - if self.zoom_in.contains(&p) { - Some(Hit::ZoomIn) - } else if self.zoom_out.contains(&p) { - Some(Hit::ZoomOut) - } else if self.reset.contains(&p) { - Some(Hit::ResetZoom) - } else if self.fit_entire_graph.contains(&p) { - Some(Hit::FitEntireGraph) - } else { - None - } - } -} - -#[derive(Copy, Clone)] -enum Hit { - ZoomIn, - ZoomOut, - ResetZoom, - FitEntireGraph, -} - -fn bar_outer_size() -> (f32, f32) { - let w = 4.0 * BTN + 3.0 * GAP; - (w, BTN) -} - -fn build_layout(window_bounds: Bounds) -> ZoomControlsLayout { - let wh: f32 = window_bounds.size.height.into(); - let (_, bar_h) = bar_outer_size(); - let s = px(BTN); - let m = px(MARGIN); - - let y0 = px(wh - MARGIN - bar_h); - let x0 = m; - - let zoom_in = Bounds::new(Point::new(x0, y0), Size::new(s, s)); - let zoom_out = Bounds::new( - Point::new(px(f32::from(x0) + BTN + GAP), y0), - Size::new(s, s), - ); - let reset = Bounds::new( - Point::new(px(f32::from(x0) + 2.0 * (BTN + GAP)), y0), - Size::new(s, s), - ); - let fit_entire_graph = Bounds::new( - Point::new(px(f32::from(x0) + 3.0 * (BTN + GAP)), y0), - Size::new(s, s), - ); - - ZoomControlsLayout { - zoom_in, - zoom_out, - reset, - fit_entire_graph, - } -} - -struct ViewportZoomCommand { - from_zoom: f32, - from_offset: Point, - to_zoom: f32, - to_offset: Point, -} - -impl Command for ViewportZoomCommand { - fn name(&self) -> &'static str { - "viewport_zoom" - } - - fn execute(&mut self, ctx: &mut CommandContext) { - ctx.set_zoom(self.to_zoom); - ctx.set_offset(self.to_offset); - } - - fn undo(&mut self, ctx: &mut CommandContext) { - ctx.set_zoom(self.from_zoom); - ctx.set_offset(self.from_offset); - } - - fn to_ops(&self, ctx: &mut crate::CommandContext) -> Vec { - ctx.set_zoom(self.to_zoom); - ctx.set_offset(self.to_offset); - vec![] - } -} - -fn apply_zoom(ctx: &mut PluginContext, anchor_screen: Point, to_zoom: f32) { - let to_zoom = to_zoom.clamp(ZOOM_MIN, ZOOM_MAX); - let from_zoom = ctx.zoom(); - let from_offset = ctx.offset(); - if (from_zoom - to_zoom).abs() < 1e-5 { - return; - } - let anchor_world = ctx.screen_to_world(anchor_screen); - let wx: f32 = anchor_world.x.into(); - let wy: f32 = anchor_world.y.into(); - let ax: f32 = anchor_screen.x.into(); - let ay: f32 = anchor_screen.y.into(); - let to_offset = Point::new(px(ax - wx * to_zoom), px(ay - wy * to_zoom)); - ctx.execute_command(ViewportZoomCommand { - from_zoom, - from_offset, - to_zoom, - to_offset, - }); -} - -fn window_center_screen(ctx: &PluginContext) -> Option> { - let wb = ctx.window_bounds()?; - let cx: f32 = (wb.size.width / 2.0).into(); - let cy: f32 = (wb.size.height / 2.0).into(); - Some(Point::new(px(cx), px(cy))) -} - -fn zoom_by_factor(ctx: &mut PluginContext, factor: f32) { - let Some(center) = window_center_screen(ctx) else { - return; - }; - apply_zoom(ctx, center, ctx.zoom_scaled_by(factor)); -} - -fn reset_zoom(ctx: &mut PluginContext) { - let Some(center) = window_center_screen(ctx) else { - return; - }; - apply_zoom(ctx, center, 1.0); -} - -/// Bottom-left **+** / **−** / **↺** / **⛶** (fit all); priority **128** so clicks beat canvas selection. -pub struct ZoomControlsPlugin { - last_layout: Option, -} - -impl Default for ZoomControlsPlugin { - fn default() -> Self { - Self::new() - } -} - -impl ZoomControlsPlugin { - pub fn new() -> Self { - Self { last_layout: None } - } -} - -impl Plugin for ZoomControlsPlugin { - fn name(&self) -> &'static str { - "zoom_controls" - } - - fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult { - if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event - && ev.button == MouseButton::Left - && let Some(ref layout) = self.last_layout - && let Some(hit) = layout.hit(ev.position) - { - match hit { - Hit::ZoomIn => zoom_by_factor(ctx, ZOOM_STEP), - Hit::ZoomOut => zoom_by_factor(ctx, 1.0 / ZOOM_STEP), - Hit::ResetZoom => reset_zoom(ctx), - Hit::FitEntireGraph => fit_entire_graph(ctx), - } - ctx.notify(); - return EventResult::Stop; - } - EventResult::Continue - } - - fn priority(&self) -> i32 { - 128 - } - - fn render_layer(&self) -> RenderLayer { - RenderLayer::Overlay - } - - fn render(&mut self, ctx: &mut RenderContext) -> Option { - let win = ctx.window_bounds().unwrap_or_else(|| { - let vs = ctx.window.viewport_size(); - Bounds::new(Point::new(px(0.0), px(0.0)), Size::new(vs.width, vs.height)) - }); - let wh: f32 = win.size.height.into(); - let (bar_w, bar_h) = bar_outer_size(); - if wh < MARGIN + bar_h + 1.0 { - self.last_layout = None; - return None; - } - - let layout = build_layout(win); - self.last_layout = Some(layout); - - let bar_w_px = px(bar_w); - - let btn_bg = ctx.theme.zoom_controls_background; - let btn_border = ctx.theme.zoom_controls_border; - let btn_text = ctx.theme.zoom_controls_text; - - let mk_btn = move |label: &'static str| { - div() - .w(px(BTN)) - .h(px(BTN)) - .flex() - .items_center() - .justify_center() - .rounded(px(6.0)) - .bg(rgb(btn_bg)) - .border_1() - .border_color(rgb(btn_border)) - .text_sm() - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(btn_text)) - .child(label) - }; - - Some( - div() - .absolute() - .size_full() - .child( - div() - .absolute() - .bottom(px(MARGIN)) - .left(px(MARGIN)) - .w(bar_w_px) - .h(px(bar_h)) - .flex() - .flex_row() - .gap(px(GAP)) - .items_center() - .children(vec![ - mk_btn(LABEL_ZOOM_IN), - mk_btn(LABEL_ZOOM_OUT), - mk_btn(LABEL_RESET_ZOOM), - mk_btn(LABEL_FIT_ENTIRE_GRAPH), - ]), - ) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod command_interop_tests { - use gpui::{Point, px}; - - use crate::{Graph, command_interop::assert_command_interop}; - - use super::ViewportZoomCommand; - - #[test] - fn viewport_zoom_command_interop() { - let base = Graph::new(); - let cmd = ViewportZoomCommand { - from_zoom: 1.0, - from_offset: Point::new(px(0.0), px(0.0)), - to_zoom: 1.25, - to_offset: Point::new(px(5.0), px(6.0)), - }; - assert_command_interop( - &base, - || { - Box::new(ViewportZoomCommand { - from_zoom: cmd.from_zoom, - from_offset: cmd.from_offset, - to_zoom: cmd.to_zoom, - to_offset: cmd.to_offset, - }) - }, - "ViewportZoomCommand", - ); - } -} diff --git a/crates/ferrum-flow/src/port_screen.rs b/crates/ferrum-flow/src/port_screen.rs deleted file mode 100644 index c47ecff147..0000000000 --- a/crates/ferrum-flow/src/port_screen.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Screen-space port layout for canvas rendering ([`PortScreenFrame`]). - -use gpui::{ - Div, ElementId, InteractiveElement as _, Pixels, Point, Size, Stateful, Styled as _, div, -}; - -use crate::PortId; - -/// Screen-space layout for one port after the viewport transform. -/// -/// Prefer resolving via [`crate::plugin::RenderContext::port_screen_frame`] (or -/// [`crate::plugin::PluginContext::port_screen_frame`] during interaction). -/// -/// Typical patterns: -/// - Default disc: [`Self::anchor_div`] then chain `.rounded_full()`, colors, borders. -/// - Custom chrome: build children inside [`Self::anchor_div`], or use [`Self::center`] -/// / [`Self::scaled_size`] for labels, sockets, multi-layer ports. -/// - Larger hit target: [`Self::anchor_div`] then override `.w`/`.h` while keeping [`Self::center`]. -#[derive(Clone, Copy, Debug)] -pub struct PortScreenFrame { - /// Port center in screen pixels (aligned with edge curve endpoints). - pub center: Point, - /// Logical port size from graph data (same units as on the node card). - pub size: Size, - pub zoom: f32, - pub(crate) port_id: PortId, -} - -impl PortScreenFrame { - /// `size` scaled by [`Self::zoom`], i.e. the on-screen port box size. - pub fn scaled_size(&self) -> Size { - let z = self.zoom; - Size { - width: self.size.width * z, - height: self.size.height * z, - } - } - - /// Top-left of the axis-aligned rectangle centered on [`Self::center`]. - pub fn origin(&self) -> Point { - let s = self.scaled_size(); - Point::new( - self.center.x - s.width / 2.0, - self.center.y - s.height / 2.0, - ) - } - - /// `absolute` container covering the default port hit box; chain GPUI styles and children. - pub fn anchor_div(self) -> Stateful
{ - let s = self.scaled_size(); - let o = self.origin(); - div() - .id(ElementId::Uuid(*self.port_id.as_uuid())) - .absolute() - .left(o.x) - .top(o.y) - .w(s.width) - .h(s.height) - } -} diff --git a/crates/ferrum-flow/src/shared_state.rs b/crates/ferrum-flow/src/shared_state.rs deleted file mode 100644 index 1e02c106e1..0000000000 --- a/crates/ferrum-flow/src/shared_state.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Type-erased map for data shared between plugins on one [`crate::canvas::FlowCanvas`]. -//! -//! Store values under their concrete Rust type (`TypeId`). Each type may appear at most once. -//! Prefer newtype wrappers per feature to avoid collisions (e.g. `struct MyPluginState(u32)`). - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt; - -/// Keyed by [`TypeId`]; values must be `'static` and [`Send`]. -pub struct SharedState { - inner: HashMap>, -} - -impl fmt::Debug for SharedState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SharedState") - .field("len", &self.inner.len()) - .finish() - } -} - -impl SharedState { - pub(crate) fn new() -> Self { - Self { - inner: HashMap::new(), - } - } - - /// Inserts a value, returning the previous one of the same type if any. - pub fn insert(&mut self, value: T) -> Option { - let id = TypeId::of::(); - let old = self.inner.remove(&id); - self.inner.insert(id, Box::new(value)); - old.and_then(|b| b.downcast::().ok().map(|b| *b)) - } - - pub fn get(&self) -> Option<&T> { - self.inner.get(&TypeId::of::())?.downcast_ref() - } - - pub fn get_mut(&mut self) -> Option<&mut T> { - self.inner.get_mut(&TypeId::of::())?.downcast_mut() - } - - pub fn remove(&mut self) -> Option { - self.inner - .remove(&TypeId::of::()) - .and_then(|b| b.downcast::().ok().map(|b| *b)) - } - - pub fn contains(&self) -> bool { - self.inner.contains_key(&TypeId::of::()) - } -} diff --git a/crates/ferrum-flow/src/theme.rs b/crates/ferrum-flow/src/theme.rs deleted file mode 100644 index d735ffccc6..0000000000 --- a/crates/ferrum-flow/src/theme.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Canvas-wide visual tokens. Plugins can replace or tweak values in -//! [`crate::plugin::InitPluginContext::theme`] / [`crate::plugin::PluginContext::theme`]. -//! -//! Colors are `u32` in **GPUI `rgb` / `rgba` layout**: `0x00RRGGBB` for opaque colors -//! (first byte unused by [`gpui::rgb`]), and `0xRRGGBBAA` for [`gpui::rgba`] fills. - -/// Default canvas chrome: node cards, grid, edges, selection marquee. -#[derive(Debug, Clone, PartialEq)] -pub struct FlowTheme { - /// Default node card background ([`gpui::rgb`]). - pub node_card_background: u32, - /// Default node card border when not selected. - pub node_card_border: u32, - /// Default node card border when selected. - pub node_card_border_selected: u32, - - /// Unknown node type card background. - pub undefined_node_background: u32, - /// Unknown node type card border. - pub undefined_node_border: u32, - - /// Primary label on default node cards. - pub node_caption_text: u32, - /// Label on undefined-type node cards. - pub undefined_node_caption_text: u32, - - /// Default circular port fill ([`NodeRenderer::port_render`](crate::NodeRenderer::port_render); - /// layout via [`crate::plugin::RenderContext::port_screen_frame`]). - pub default_port_fill: u32, - - /// Main surface color behind the dot grid. - pub background: u32, - /// Dot color for the background grid. - pub background_grid_dot: u32, - - /// Edge curve when not selected. - pub edge_stroke: u32, - /// Edge curve when selected. - pub edge_stroke_selected: u32, - - /// Marquee / move-preview rectangle outline ([`gpui::rgb`]). - pub selection_rect_border: u32, - /// Marquee / move-preview fill ([`gpui::rgba`], e.g. `0x78A0FF4c`). - pub selection_rect_fill_rgba: u32, - - /// Temporary line while dragging a link from a port. - pub port_preview_line: u32, - /// Endpoint disc while dragging a link from a port (muted so it does not overpower the canvas). - pub port_preview_dot: u32, - - /// Minimap inner panel fill ([`crate::MinimapPlugin`]). - pub minimap_background: u32, - /// Minimap inner panel outline. - pub minimap_border: u32, - /// Minimap graph edges (straight segments between node centers). - pub minimap_edge: u32, - /// Minimap node rectangle fill. - pub minimap_node_fill: u32, - /// Minimap node rectangle outline. - pub minimap_node_stroke: u32, - /// Minimap viewport / visible-area frame. - pub minimap_viewport_stroke: u32, - - /// Zoom bar button fill ([`crate::ZoomControlsPlugin`]). - pub zoom_controls_background: u32, - /// Zoom bar button border. - pub zoom_controls_border: u32, - /// Zoom bar glyph color. - pub zoom_controls_text: u32, - - /// Context menu panel fill ([`crate::ContextMenuPlugin`]). - pub context_menu_background: u32, - /// Context menu panel outline. - pub context_menu_border: u32, - /// Context menu row label. - pub context_menu_text: u32, - /// Context menu shortcut hint (muted). - pub context_menu_shortcut_text: u32, - /// Context menu separator rule between rows. - pub context_menu_separator: u32, - - /// common error color. - pub error: u32, - /// common info color. - pub info: u32, - /// common success color. - pub success: u32, - /// common warning color. - pub warning: u32, -} - -impl Default for FlowTheme { - #[allow(clippy::mixed_case_hex_literals)] - fn default() -> Self { - Self { - node_card_background: 0x00FFFFFF, - node_card_border: 0x001A192B, - node_card_border_selected: 0x00FF7800, - undefined_node_background: 0x00F5F5F5, - undefined_node_border: 0x00FF9800, - node_caption_text: 0x001A192B, - undefined_node_caption_text: 0x005F6368, - default_port_fill: 0x001A192B, - background: 0x00f8f9fb, - background_grid_dot: 0x009F9FA7, - edge_stroke: 0x00b1b1b8, - edge_stroke_selected: 0x00FF7800, - selection_rect_border: 0x0078A0FF, - selection_rect_fill_rgba: 0x78A0FF4c, - port_preview_line: 0x00b1b1b8, - port_preview_dot: 0x007189a3, - minimap_background: 0x00f8f9fb, - minimap_border: 0x00b1b1b8, - minimap_edge: 0x00b1b1b8, - minimap_node_fill: 0x00FFFFFF, - minimap_node_stroke: 0x001a192b, - minimap_viewport_stroke: 0x0078a0ff, - zoom_controls_background: 0x00fcfcfc, - zoom_controls_border: 0x00c8c8d0, - zoom_controls_text: 0x001a192b, - context_menu_background: 0x00fcfcfc, - context_menu_border: 0x00c8c8d0, - context_menu_text: 0x001a192b, - context_menu_shortcut_text: 0x007a7a88, - context_menu_separator: 0x00e0e0e8, - error: 0x00FF1744, - info: 0x001F2937, - success: 0x001E8E3E, - warning: 0x00B35A00, - } - } -} - -impl FlowTheme { - /// Same as [`Default::default`]; kept for explicit call sites. - pub fn light() -> Self { - Self::default() - } -} diff --git a/crates/ferrum-flow/src/viewport.rs b/crates/ferrum-flow/src/viewport.rs deleted file mode 100644 index 11526d7128..0000000000 --- a/crates/ferrum-flow/src/viewport.rs +++ /dev/null @@ -1,192 +0,0 @@ -use gpui::{Bounds, Pixels, Point, Size, Window, px}; - -use crate::{Node, PortPosition}; - -/// Fingerprint of [`Viewport`] fields that affect [`Viewport::is_node_visible`]. -/// Used by [`crate::NodePlugin`] to avoid rescanning the full node list every frame. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct ViewportVisibilityCacheKey { - pub zoom: f32, - pub offset_x: f32, - pub offset_y: f32, - pub has_window: bool, - pub window_w: f32, - pub window_h: f32, -} - -#[derive(Debug, Clone)] -pub struct Viewport { - zoom: f32, - offset: Point, - window_bounds: Option>, -} - -impl Viewport { - pub(crate) fn new() -> Self { - Self { - zoom: 1.0, - offset: Point::new(px(0.0), px(0.0)), - window_bounds: None, - } - } - - /// Sets [`Self::window_bounds`] to the window’s drawable area (`Window::viewport_size`), - /// origin `(0, 0)`. Skips assignment when width/height are unchanged. - /// - /// Prefer this over `Window::bounds()` for hit-testing and overlay layout: the latter is in - /// global space and can be larger than the content viewport. - pub fn sync_drawable_bounds(&mut self, window: &Window) { - let vs = window.viewport_size(); - let unchanged = self - .window_bounds - .is_some_and(|b| b.size.width == vs.width && b.size.height == vs.height); - if !unchanged { - self.window_bounds = Some(Bounds::new( - Point::new(px(0.0), px(0.0)), - Size::new(vs.width, vs.height), - )); - } - } - - /// Sets [`Self::window_bounds`] to the canvas element's local drawable area. - pub fn sync_canvas_bounds(&mut self, bounds: Bounds) { - let unchanged = self.window_bounds.is_some_and(|b| { - b.size.width == bounds.size.width && b.size.height == bounds.size.height - }); - if !unchanged { - self.window_bounds = Some(Bounds::new( - Point::new(px(0.0), px(0.0)), - Size::new(bounds.size.width, bounds.size.height), - )); - } - } - - pub fn zoom(&self) -> f32 { - self.zoom - } - - pub fn set_zoom(&mut self, zoom: f32) { - self.zoom = zoom; - } - - /// Compute a new zoom value by multiplying current zoom with `factor`. - pub fn zoom_scaled_by(&self, factor: f32) -> f32 { - self.zoom * factor - } - - pub fn offset(&self) -> Point { - self.offset - } - - pub fn set_offset(&mut self, offset: Point) { - self.offset = offset; - } - - pub fn set_offset_xy(&mut self, x: Pixels, y: Pixels) { - self.offset = Point::new(x, y); - } - - pub fn translate_offset(&mut self, dx: Pixels, dy: Pixels) { - self.offset.x += dx; - self.offset.y += dy; - } - - pub fn window_bounds(&self) -> Option> { - self.window_bounds - } - - pub fn set_window_bounds(&mut self, bounds: Option>) { - self.window_bounds = bounds; - } - - /// Convert a world-space scalar length to screen-space scalar length. - pub fn world_scalar_to_screen(&self, value: f32) -> f32 { - value * self.zoom - } - - /// Convert a screen-space scalar length to world-space scalar length. - pub fn screen_scalar_to_world(&self, value: f32) -> f32 { - value / self.zoom - } - - /// Convert a world-space pixel length to screen-space pixel length. - pub fn world_length_to_screen(&self, value: Pixels) -> Pixels { - value * self.zoom - } - - /// Convert a screen-space pixel length to world-space pixel length. - pub fn screen_length_to_world(&self, value: Pixels) -> Pixels { - value / self.zoom - } - - pub fn world_to_screen(&self, p: Point) -> Point { - Point::new( - self.world_length_to_screen(p.x) + self.offset.x, - self.world_length_to_screen(p.y) + self.offset.y, - ) - } - - pub fn screen_to_world(&self, p: Point) -> Point { - Point::new( - self.screen_length_to_world(p.x - self.offset.x), - self.screen_length_to_world(p.y - self.offset.y), - ) - } - - /// Bezier control point for an edge tangent at a port direction. - pub fn edge_control_point( - &self, - source: Point, - position: PortPosition, - ) -> Point { - match position { - PortPosition::Top => { - source - Point::new(px(0.0), px(self.world_scalar_to_screen(50.0))) - } - PortPosition::Left => { - source - Point::new(px(self.world_scalar_to_screen(50.0)), px(0.0)) - } - PortPosition::Right => { - source + Point::new(px(self.world_scalar_to_screen(50.0)), px(0.0)) - } - PortPosition::Bottom => { - source + Point::new(px(0.0), px(self.world_scalar_to_screen(50.0))) - } - } - } - - pub fn is_node_visible(&self, node: &Node) -> bool { - let Some(window_bounds) = self.window_bounds else { - return false; - }; - - let screen = self.world_to_screen(node.point()); - let size = *node.size_ref(); - - screen.x + self.world_length_to_screen(size.width) > px(0.0) - && screen.x < window_bounds.size.width - && screen.y + self.world_length_to_screen(size.height) > px(0.0) - && screen.y < window_bounds.size.height - } - - pub(crate) fn visibility_cache_key(&self) -> ViewportVisibilityCacheKey { - match self.window_bounds { - Some(b) => ViewportVisibilityCacheKey { - zoom: self.zoom, - offset_x: self.offset.x.into(), - offset_y: self.offset.y.into(), - has_window: true, - window_w: b.size.width.into(), - window_h: b.size.height.into(), - }, - None => ViewportVisibilityCacheKey { - zoom: self.zoom, - offset_x: self.offset.x.into(), - offset_y: self.offset.y.into(), - has_window: false, - window_w: 0.0, - window_h: 0.0, - }, - } - } -} diff --git a/crates/one_ui/locales/one_ui.yml b/crates/one_ui/locales/one_ui.yml deleted file mode 100644 index 63f1c3e949..0000000000 --- a/crates/one_ui/locales/one_ui.yml +++ /dev/null @@ -1 +0,0 @@ -en: diff --git a/crates/one_ui/src/edit_table/delegate.rs b/crates/one_ui/src/edit_table/delegate.rs index c047088e15..87b4400f04 100644 --- a/crates/one_ui/src/edit_table/delegate.rs +++ b/crates/one_ui/src/edit_table/delegate.rs @@ -44,22 +44,14 @@ impl CellEditor { .h_full() .text_base() .appearance(false) - .px_2() - .py_1() - .ml(px(1.)) - .mt(px(3.)) - .items_center() + .bare() .into_any_element(), CellEditor::NumberInput(input) => Input::new(input) .w_full() .h_full() .text_base() .appearance(false) - .px_2() - .py_1() - .ml(px(1.)) - .mt(px(1.)) - .items_center() + .bare() .into_any_element(), CellEditor::DatePicker(picker) => DatePicker::new(picker) .w_full() diff --git a/crates/one_ui/src/edit_table/state.rs b/crates/one_ui/src/edit_table/state.rs index efa383d23d..b8fb4fe5c8 100644 --- a/crates/one_ui/src/edit_table/state.rs +++ b/crates/one_ui/src/edit_table/state.rs @@ -2036,20 +2036,31 @@ where this.bg(cx.theme().warning.opacity(0.15)) }); + // 统一布局:编辑和显示模式使用相同的容器 padding + cell = cell.table_cell_size(self.options.size); + + let size_pad = self.options.size.table_cell_padding(); + let (target_pt, target_pb, target_pl, target_pr) = match col_padding { + Some(p) => (p.top, p.bottom, p.left, p.right), + None => ( + size_pad.top, + size_pad.bottom, + size_pad.left, + size_pad.right, + ), + }; + + cell = cell + .pt(target_pt) + .pb(target_pb) + .pl(target_pl) + .pr(target_pr); + + // 编辑模式:嵌入轻量编辑器(无自带样式,由容器控制布局) if is_editing { if let Some(editor) = &self.editing_input { cell = cell.child(editor.render(window, cx)); } - } else { - cell = cell.table_cell_size(self.options.size); - cell = match col_padding { - Some(padding) => cell - .pl(padding.left) - .pr(padding.right) - .pt(padding.top) - .pb(padding.bottom), - None => cell, - }; } cell diff --git a/crates/remote_file_editor/locales/remote_file_editor.yml b/crates/remote_file_editor/locales/remote_file_editor.yml index 933185e79f..a3f1c3829b 100644 --- a/crates/remote_file_editor/locales/remote_file_editor.yml +++ b/crates/remote_file_editor/locales/remote_file_editor.yml @@ -26,6 +26,10 @@ RemoteFileEditor: en: Soft Wrap zh-CN: 自动换行 zh-HK: 自動換行 + close_tab: + en: Close Tab + zh-CN: 关闭页签 + zh-HK: 關閉頁籤 discard: en: Discard zh-CN: 放弃更改 diff --git a/crates/remote_file_editor/src/close_guard.rs b/crates/remote_file_editor/src/close_guard.rs index 0422298160..1afe57b8d0 100644 --- a/crates/remote_file_editor/src/close_guard.rs +++ b/crates/remote_file_editor/src/close_guard.rs @@ -15,9 +15,42 @@ pub fn decide_close_intercept(is_dirty: bool, prompt_open: bool) -> CloseInterce } } +pub fn find_tab_index(paths: &[String], remote_path: &str) -> Option { + paths.iter().position(|path| path == remote_path) +} + +pub fn active_index_after_open(paths: &[String], remote_path: &str) -> usize { + find_tab_index(paths, remote_path).unwrap_or(paths.len()) +} + +pub fn active_index_after_close( + active_index: usize, + closed_index: usize, + tab_count: usize, +) -> Option { + if tab_count <= 1 || closed_index >= tab_count { + return None; + } + + if closed_index < active_index { + Some(active_index - 1) + } else if closed_index == active_index && active_index >= tab_count - 1 { + Some(active_index - 1) + } else { + Some(active_index) + } +} + +pub fn has_dirty_tabs(dirty_tabs: &[bool]) -> bool { + dirty_tabs.iter().any(|dirty| *dirty) +} + #[cfg(test)] mod tests { - use super::{CloseIntercept, decide_close_intercept}; + use super::{ + CloseIntercept, active_index_after_close, active_index_after_open, decide_close_intercept, + find_tab_index, has_dirty_tabs, + }; #[test] fn allows_close_when_editor_is_clean() { @@ -33,4 +66,56 @@ mod tests { fn ignores_repeated_close_while_prompt_is_open() { assert_eq!(decide_close_intercept(true, true), CloseIntercept::Ignore); } + + #[test] + fn finds_existing_tab_index_by_remote_path() { + let paths = vec!["/tmp/a.txt".to_string(), "/tmp/b.txt".to_string()]; + + assert_eq!(find_tab_index(&paths, "/tmp/b.txt"), Some(1)); + } + + #[test] + fn returns_next_index_for_new_remote_path() { + let paths = vec!["/tmp/a.txt".to_string(), "/tmp/b.txt".to_string()]; + + assert_eq!(active_index_after_open(&paths, "/tmp/c.txt"), 2); + } + + #[test] + fn reuses_existing_index_for_existing_remote_path() { + let paths = vec!["/tmp/a.txt".to_string(), "/tmp/b.txt".to_string()]; + + assert_eq!(active_index_after_open(&paths, "/tmp/a.txt"), 0); + } + + #[test] + fn keeps_active_index_when_closing_tab_after_active_tab() { + assert_eq!(active_index_after_close(0, 2, 3), Some(0)); + } + + #[test] + fn shifts_active_index_left_when_closing_tab_before_active_tab() { + assert_eq!(active_index_after_close(2, 0, 3), Some(1)); + } + + #[test] + fn activates_left_tab_when_closing_last_active_tab() { + assert_eq!(active_index_after_close(2, 2, 3), Some(1)); + } + + #[test] + fn keeps_same_index_when_closing_middle_active_tab_with_right_neighbor() { + assert_eq!(active_index_after_close(1, 1, 3), Some(1)); + } + + #[test] + fn returns_none_when_closing_last_remaining_tab() { + assert_eq!(active_index_after_close(0, 0, 1), None); + } + + #[test] + fn detects_any_dirty_tab() { + assert!(has_dirty_tabs(&[false, true, false])); + assert!(!has_dirty_tabs(&[false, false])); + } } diff --git a/crates/remote_file_editor/src/editor_window.rs b/crates/remote_file_editor/src/editor_window.rs index 6d7c4a77cf..f7b17bbe8b 100644 --- a/crates/remote_file_editor/src/editor_window.rs +++ b/crates/remote_file_editor/src/editor_window.rs @@ -2,25 +2,29 @@ use crate::file_policy::{ EditorMode, FilePolicy, MAX_EDITABLE_FILE_SIZE, decode_text_content, determine_file_policy, }; use crate::language::language_for_path; -use crate::{CloseIntercept, decide_close_intercept}; +use crate::{ + CloseIntercept, active_index_after_close, active_index_after_open, decide_close_intercept, +}; use gpui::{ - App, AppContext, Bounds, Context, Entity, InteractiveElement as _, IntoElement, KeyBinding, - ParentElement, PromptLevel, Render, Size as GpuiSize, Styled, Window, WindowBounds, WindowKind, - WindowOptions, actions, div, px, size, + AnyWindowHandle, App, AppContext, Context, Entity, InteractiveElement as _, IntoElement, + KeyBinding, ParentElement, PromptLevel, Render, Styled, WeakEntity, Window, actions, div, px, }; use gpui_component::{ - ActiveTheme as _, Disableable as _, Root, Selectable as _, Sizable as _, Size, TitleBar, - WindowExt, + ActiveTheme as _, Disableable as _, Selectable as _, Sizable as _, Size, TitleBar, WindowExt, button::Button, h_flex, input::{Input, InputEvent, InputState, Search}, notification::Notification, + tab::{Tab, TabBar}, v_flex, }; -use one_core::gpui_tokio::Tokio; +use one_core::{ + gpui_tokio::Tokio, + popup_window::{PopupWindowOptions, open_popup_window}, +}; use rust_i18n::t; use sftp::{RusshSftpClient, SftpClient}; -use std::sync::{Arc, Once}; +use std::sync::{Arc, Mutex as StdMutex, Once, OnceLock}; use tokio::sync::Mutex; actions!(remote_file_editor, [OpenSearch, OpenReplace]); @@ -36,60 +40,86 @@ const REMOTE_EDITOR_REPLACE_SHORTCUT: &str = "cmd-r"; const REMOTE_EDITOR_REPLACE_SHORTCUT: &str = "ctrl-r"; static REMOTE_EDITOR_KEYBINDINGS_INIT: Once = Once::new(); +static REMOTE_EDITOR_WINDOW: OnceLock>> = OnceLock::new(); + +#[derive(Clone)] +struct RemoteEditorWindowRef { + window: AnyWindowHandle, + view: WeakEntity, +} pub fn open_remote_file_editor( remote_path: String, client: Arc>, + window: &mut Window, cx: &mut Context, ) { init_keybindings(cx); - let title = t!( - "RemoteFileEditor.title", - name = display_name_from_path(&remote_path) - ) - .to_string(); - cx.spawn(async move |_this, cx| { - let title = title.clone(); - let remote_path_for_log = remote_path.clone(); - let result = cx.update(|cx| { - let mut window_size = size(px(960.0), px(720.0)); - if let Some(display) = cx.primary_display() { - let display_size = display.bounds().size; - window_size.width = window_size.width.min(display_size.width * 0.85); - window_size.height = window_size.height.min(display_size.height * 0.85); - } - let window_bounds = Bounds::centered(None, window_size, cx); - let window_opts = WindowOptions { - window_bounds: Some(WindowBounds::Windowed(window_bounds)), - titlebar: Some(TitleBar::title_bar_options()), - window_min_size: Some(GpuiSize { - width: px(640.0), - height: px(480.0), - }), - kind: WindowKind::Normal, - #[cfg(target_os = "linux")] - window_background: gpui::WindowBackgroundAppearance::Transparent, - #[cfg(target_os = "linux")] - window_decorations: Some(gpui::WindowDecorations::Client), - ..Default::default() - }; + let _ = remote_path.clone(); + if let Ok(true) = open_in_existing_window(remote_path.clone(), cx) { + return; + } + + let title = editor_window_title(&remote_path); + open_popup_window( + window, + PopupWindowOptions::new(title).size(960.0, 720.0).min_width(640.0).min_height(480.0), + move |window, cx| { + let view = cx.new(|cx| { + RemoteFileEditorWindow::new(remote_path, client, window, cx) + }); + set_editor_window(RemoteEditorWindowRef { + window: window.window_handle(), + view: view.downgrade(), + }); + view + }, + cx, + ); +} - cx.open_window(window_opts, move |window, cx| { - window.activate_window(); - window.set_window_title(&title); - let view = - cx.new(|cx| RemoteFileEditorWindow::new(remote_path, client, window, cx)); - cx.new(|cx| Root::new(view, window, cx)) - })?; +fn open_in_existing_window(remote_path: String, cx: &mut App) -> anyhow::Result { + let Some(editor_window) = current_editor_window() else { + return Ok(false); + }; - Ok::<_, anyhow::Error>(()) - }); + let result = cx.update_window(editor_window.window, |_, window, cx| { + window.activate_window(); + editor_window + .view + .update(cx, |this, cx| { + this.open_or_focus_tab(remote_path, window, cx); + }) + .is_ok() + }); - if let Err(error) = result { - tracing::error!(path = %remote_path_for_log, ?error, "failed to open remote file editor"); + match result { + Ok(true) => Ok(true), + Ok(false) | Err(_) => { + clear_editor_window(); + Ok(false) } - }) - .detach(); + } +} + +fn editor_window_slot() -> &'static StdMutex> { + REMOTE_EDITOR_WINDOW.get_or_init(|| StdMutex::new(None)) +} + +fn current_editor_window() -> Option { + editor_window_slot().lock().ok()?.clone() +} + +fn set_editor_window(window: RemoteEditorWindowRef) { + if let Ok(mut slot) = editor_window_slot().lock() { + *slot = Some(window); + } +} + +fn clear_editor_window() { + if let Ok(mut slot) = editor_window_slot().lock() { + *slot = None; + } } fn init_keybindings(cx: &mut App) { @@ -117,6 +147,14 @@ fn replace_shortcut() -> &'static str { REMOTE_EDITOR_REPLACE_SHORTCUT } +fn editor_window_title(remote_path: &str) -> String { + t!( + "RemoteFileEditor.title", + name = display_name_from_path(remote_path) + ) + .to_string() +} + struct LoadedFile { text: String, policy: FilePolicy, @@ -124,10 +162,16 @@ struct LoadedFile { language: String, } -struct RemoteFileEditorWindow { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PendingCloseAction { + Window, + Tab(usize), +} + +struct RemoteEditorTab { + id: u64, remote_path: String, display_name: String, - client: Arc>, editor: Option>, subscriptions: Vec, saved_text: String, @@ -136,23 +180,16 @@ struct RemoteFileEditorWindow { loading: bool, saving: bool, soft_wrap: bool, - close_prompt_open: bool, - close_after_save: bool, status_message: String, load_error: Option, } -impl RemoteFileEditorWindow { - fn new( - remote_path: String, - client: Arc>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let mut this = Self { +impl RemoteEditorTab { + fn new(id: u64, remote_path: String) -> Self { + Self { + id, display_name: display_name_from_path(&remote_path), remote_path, - client, editor: None, subscriptions: Vec::new(), saved_text: String::new(), @@ -164,13 +201,54 @@ impl RemoteFileEditorWindow { loading: true, saving: false, soft_wrap: false, - close_prompt_open: false, - close_after_save: false, status_message: t!("RemoteFileEditor.status.loading").to_string(), load_error: None, + } + } + + fn is_dirty(&self, cx: &App) -> bool { + self.editor + .as_ref() + .map(|editor| editor.read(cx).text() != self.saved_text.as_str()) + .unwrap_or(false) + } + + fn policy_label(&self) -> String { + match self.policy.mode { + EditorMode::Code => t!("RemoteFileEditor.policy.code").to_string(), + EditorMode::PlainText => t!("RemoteFileEditor.policy.plain_text").to_string(), + } + } +} + +struct RemoteFileEditorWindow { + client: Arc>, + tabs: Vec, + active_tab: usize, + close_prompt_open: bool, + pending_close_action: Option, + close_window_after_saves: bool, + next_tab_id: u64, +} + +impl RemoteFileEditorWindow { + fn new( + remote_path: String, + client: Arc>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut this = Self { + client, + tabs: Vec::new(), + active_tab: 0, + close_prompt_open: false, + pending_close_action: None, + close_window_after_saves: false, + next_tab_id: 1, }; this.register_close_guard(window, cx); - this.reload(window, cx); + this.open_or_focus_tab(remote_path, window, cx); this } @@ -182,25 +260,84 @@ impl RemoteFileEditorWindow { }); } + fn open_or_focus_tab( + &mut self, + remote_path: String, + window: &mut Window, + cx: &mut Context, + ) { + let paths = self.tab_paths(); + let active_index = active_index_after_open(&paths, &remote_path); + if active_index == self.tabs.len() { + let tab_id = self.next_tab_id; + self.next_tab_id += 1; + self.tabs.push(RemoteEditorTab::new(tab_id, remote_path)); + self.active_tab = active_index; + self.reload_tab(active_index, window, cx); + } else { + self.active_tab = active_index; + self.focus_editor(window, cx); + cx.notify(); + } + self.update_window_title(window); + } + + fn tab_paths(&self) -> Vec { + self.tabs + .iter() + .map(|tab| tab.remote_path.clone()) + .collect() + } + + fn tab_index_by_identity(&self, tab_id: u64, remote_path: &str) -> Option { + self.tabs + .iter() + .position(|tab| tab.id == tab_id && tab.remote_path == remote_path) + } + + fn active_tab(&self) -> Option<&RemoteEditorTab> { + self.tabs.get(self.active_tab) + } + + fn active_tab_mut(&mut self) -> Option<&mut RemoteEditorTab> { + self.tabs.get_mut(self.active_tab) + } + + fn update_window_title(&self, window: &mut Window) { + if let Some(tab) = self.active_tab() { + window.set_window_title(&editor_window_title(&tab.remote_path)); + } + } + fn reload(&mut self, window: &mut Window, cx: &mut Context) { - self.loading = true; - self.load_error = None; - self.status_message = t!("RemoteFileEditor.status.loading").to_string(); + self.reload_tab(self.active_tab, window, cx); + } + + fn reload_tab(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; + + tab.loading = true; + tab.load_error = None; + tab.status_message = t!("RemoteFileEditor.status.loading").to_string(); cx.notify(); - let remote_path = self.remote_path.clone(); + let tab_id = tab.id; + let remote_path = tab.remote_path.clone(); + let task_remote_path = remote_path.clone(); let client = self.client.clone(); let task = Tokio::spawn(cx, async move { let bytes = { let mut client = client.lock().await; client - .read_file(&remote_path, MAX_EDITABLE_FILE_SIZE) + .read_file(&task_remote_path, MAX_EDITABLE_FILE_SIZE) .await? }; let file_size = bytes.len(); let policy = determine_file_policy(file_size)?; let text = decode_text_content(&bytes)?; - let language = language_for_path(&remote_path, policy.is_large_file).to_string(); + let language = language_for_path(&task_remote_path, policy.is_large_file).to_string(); Ok::<_, anyhow::Error>(LoadedFile { text, policy, @@ -214,24 +351,20 @@ impl RemoteFileEditorWindow { .spawn(cx, async move |cx| match task.await { Ok(Ok(loaded)) => { let _ = view.update_in(cx, |this, window, cx| { - this.apply_loaded_file(loaded, window, cx); + this.apply_loaded_file(tab_id, &remote_path, loaded, window, cx); }); } Ok(Err(error)) => { let message = error.to_string(); let _ = view.update_in(cx, |this, window, cx| { - this.loading = false; - this.load_error = Some(message.clone()); - this.status_message = t!("RemoteFileEditor.status.load_failed").to_string(); + this.apply_load_error(tab_id, &remote_path, message.clone(), cx); window.push_notification(Notification::error(message), cx); }); } Err(error) => { let message = error.to_string(); let _ = view.update_in(cx, |this, window, cx| { - this.loading = false; - this.load_error = Some(message.clone()); - this.status_message = t!("RemoteFileEditor.status.load_failed").to_string(); + this.apply_load_error(tab_id, &remote_path, message.clone(), cx); window.push_notification(Notification::error(message), cx); }); } @@ -241,10 +374,18 @@ impl RemoteFileEditorWindow { fn apply_loaded_file( &mut self, + tab_id: u64, + remote_path: &str, loaded: LoadedFile, window: &mut Window, cx: &mut Context, ) { + let Some(index) = self.tab_index_by_identity(tab_id, remote_path) else { + return; + }; + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; let LoadedFile { text, policy, @@ -253,7 +394,7 @@ impl RemoteFileEditorWindow { } = loaded; let initial_text = text.clone(); - let soft_wrap = self.soft_wrap; + let soft_wrap = tab.soft_wrap; let editor = cx.new(|cx| { let mut state = InputState::new(window, cx) .code_editor(language) @@ -264,8 +405,8 @@ impl RemoteFileEditorWindow { state }); - self.subscriptions.clear(); - self.subscriptions.push( + tab.subscriptions.clear(); + tab.subscriptions.push( cx.subscribe(&editor, |_this, _input, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { cx.notify(); @@ -273,18 +414,20 @@ impl RemoteFileEditorWindow { }), ); - editor.update(cx, |state: &mut InputState, cx| { - state.focus(window, cx); - }); + if index == self.active_tab { + editor.update(cx, |state: &mut InputState, cx| { + state.focus(window, cx); + }); + } - self.editor = Some(editor); - self.saved_text = text; - self.file_size = file_size; - self.policy = policy; - self.loading = false; - self.saving = false; - self.load_error = None; - self.status_message = if policy.is_large_file { + tab.editor = Some(editor); + tab.saved_text = text; + tab.file_size = file_size; + tab.policy = policy; + tab.loading = false; + tab.saving = false; + tab.load_error = None; + tab.status_message = if policy.is_large_file { t!("RemoteFileEditor.status.loaded_plain_text").to_string() } else { t!("RemoteFileEditor.status.loaded").to_string() @@ -292,30 +435,64 @@ impl RemoteFileEditorWindow { cx.notify(); } + fn apply_load_error( + &mut self, + tab_id: u64, + remote_path: &str, + message: String, + cx: &mut Context, + ) { + let Some(index) = self.tab_index_by_identity(tab_id, remote_path) else { + return; + }; + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; + tab.loading = false; + tab.load_error = Some(message); + tab.status_message = t!("RemoteFileEditor.status.load_failed").to_string(); + cx.notify(); + } + fn save(&mut self, close_after_save: bool, window: &mut Window, cx: &mut Context) { - self.close_after_save |= close_after_save; - let Some(editor) = self.editor.clone() else { - if self.close_after_save { - self.close_after_save = false; - window.remove_window(); + self.save_tab(self.active_tab, close_after_save, window, cx); + } + + fn save_tab( + &mut self, + index: usize, + close_after_save: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; + let Some(editor) = tab.editor.clone() else { + if close_after_save { + self.close_clean_tab(index, window, cx); } return; }; - if self.saving { + if tab.saving { return; } let text = editor.read(cx).text().to_string(); - self.saving = true; - self.status_message = t!("RemoteFileEditor.status.saving").to_string(); + tab.saving = true; + tab.status_message = t!("RemoteFileEditor.status.saving").to_string(); cx.notify(); - let remote_path = self.remote_path.clone(); + let tab_id = tab.id; + let remote_path = tab.remote_path.clone(); + let task_remote_path = remote_path.clone(); let client = self.client.clone(); let task = Tokio::spawn(cx, async move { let mut client = client.lock().await; - client.write_file(&remote_path, text.as_bytes()).await?; + client + .write_file(&task_remote_path, text.as_bytes()) + .await?; Ok::<_, anyhow::Error>(text) }); @@ -324,40 +501,27 @@ impl RemoteFileEditorWindow { .spawn(cx, async move |cx| match task.await { Ok(Ok(saved_text)) => { let _ = view.update_in(cx, |this, window, cx| { - this.saved_text = saved_text; - this.file_size = this.saved_text.len(); - this.saving = false; - this.status_message = t!("RemoteFileEditor.status.saved").to_string(); - let close_after_save = this.close_after_save; - this.close_after_save = false; - if close_after_save { - window.remove_window(); - } else { - window.push_notification( - Notification::success( - t!("RemoteFileEditor.notification.saved").to_string(), - ), - cx, - ); - } - cx.notify(); + this.apply_saved_file( + tab_id, + &remote_path, + saved_text, + close_after_save, + window, + cx, + ); }); } Ok(Err(error)) => { let message = error.to_string(); let _ = view.update_in(cx, |this, window, cx| { - this.saving = false; - this.close_after_save = false; - this.status_message = t!("RemoteFileEditor.status.save_failed").to_string(); + this.apply_save_error(tab_id, &remote_path, message.clone(), cx); window.push_notification(Notification::error(message), cx); }); } Err(error) => { let message = error.to_string(); let _ = view.update_in(cx, |this, window, cx| { - this.saving = false; - this.close_after_save = false; - this.status_message = t!("RemoteFileEditor.status.save_failed").to_string(); + this.apply_save_error(tab_id, &remote_path, message.clone(), cx); window.push_notification(Notification::error(message), cx); }); } @@ -365,19 +529,170 @@ impl RemoteFileEditorWindow { .detach(); } + fn apply_saved_file( + &mut self, + tab_id: u64, + remote_path: &str, + saved_text: String, + close_after_save: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(index) = self.tab_index_by_identity(tab_id, remote_path) else { + return; + }; + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; + tab.saved_text = saved_text; + tab.file_size = tab.saved_text.len(); + tab.saving = false; + tab.status_message = t!("RemoteFileEditor.status.saved").to_string(); + + if self.close_window_after_saves && !self.has_dirty_tabs(cx) { + self.close_window_after_saves = false; + clear_editor_window(); + window.remove_window(); + } else if close_after_save { + self.close_clean_tab(index, window, cx); + } else { + window.push_notification( + Notification::success(t!("RemoteFileEditor.notification.saved").to_string()), + cx, + ); + cx.notify(); + } + } + + fn apply_save_error( + &mut self, + tab_id: u64, + remote_path: &str, + _message: String, + cx: &mut Context, + ) { + let Some(index) = self.tab_index_by_identity(tab_id, remote_path) else { + return; + }; + let Some(tab) = self.tabs.get_mut(index) else { + return; + }; + tab.saving = false; + tab.status_message = t!("RemoteFileEditor.status.save_failed").to_string(); + self.close_window_after_saves = false; + cx.notify(); + } + fn handle_window_should_close(&mut self, window: &mut Window, cx: &mut Context) -> bool { - match decide_close_intercept(self.is_dirty(cx), self.close_prompt_open) { - CloseIntercept::Allow => true, + match decide_close_intercept(self.has_dirty_tabs(cx), self.close_prompt_open) { + CloseIntercept::Allow => { + clear_editor_window(); + true + } CloseIntercept::Ignore => false, CloseIntercept::Prompt => { - self.show_unsaved_changes_prompt(window, cx); + if let Some(index) = self.first_dirty_tab(cx) { + self.active_tab = index; + self.update_window_title(window); + self.focus_editor(window, cx); + } + self.show_unsaved_changes_prompt(PendingCloseAction::Window, window, cx); false } } } - fn show_unsaved_changes_prompt(&mut self, window: &mut Window, cx: &mut Context) { + fn request_close_active_tab(&mut self, window: &mut Window, cx: &mut Context) { + self.request_close_tab(self.active_tab, window, cx); + } + + fn request_close_tab(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if index >= self.tabs.len() { + return; + } + + match decide_close_intercept(self.is_tab_dirty(index, cx), self.close_prompt_open) { + CloseIntercept::Allow => self.close_clean_tab(index, window, cx), + CloseIntercept::Ignore => {} + CloseIntercept::Prompt => { + self.show_unsaved_changes_prompt(PendingCloseAction::Tab(index), window, cx); + } + } + } + + fn close_clean_tab(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if index >= self.tabs.len() { + return; + } + + let next_active = active_index_after_close(self.active_tab, index, self.tabs.len()); + self.tabs.remove(index); + if let Some(next_active) = next_active { + self.active_tab = next_active; + self.update_window_title(window); + self.focus_editor(window, cx); + cx.notify(); + } else { + clear_editor_window(); + window.remove_window(); + } + } + + fn discard_close_action( + &mut self, + action: PendingCloseAction, + window: &mut Window, + cx: &mut Context, + ) { + match action { + PendingCloseAction::Window => { + clear_editor_window(); + window.remove_window(); + } + PendingCloseAction::Tab(index) => self.close_clean_tab(index, window, cx), + } + } + + fn save_close_action( + &mut self, + action: PendingCloseAction, + window: &mut Window, + cx: &mut Context, + ) { + match action { + PendingCloseAction::Window => self.save_dirty_tabs_and_close_window(window, cx), + PendingCloseAction::Tab(index) => self.save_tab(index, true, window, cx), + } + } + + fn save_dirty_tabs_and_close_window(&mut self, window: &mut Window, cx: &mut Context) { + let dirty_indexes = self + .tabs + .iter() + .enumerate() + .filter_map(|(index, tab)| tab.is_dirty(cx).then_some(index)) + .collect::>(); + + if dirty_indexes.is_empty() { + clear_editor_window(); + window.remove_window(); + return; + } + + self.close_window_after_saves = true; + for index in dirty_indexes { + self.save_tab(index, false, window, cx); + } + } + + fn show_unsaved_changes_prompt( + &mut self, + action: PendingCloseAction, + window: &mut Window, + cx: &mut Context, + ) { self.close_prompt_open = true; + self.pending_close_action = Some(action); let prompt_title = t!("RemoteFileEditor.prompt.unsaved_title").to_string(); let prompt_message = t!("RemoteFileEditor.prompt.unsaved_message").to_string(); let save_label = t!("RemoteFileEditor.action.save").to_string(); @@ -401,13 +716,12 @@ impl RemoteFileEditorWindow { let selection = answer.await.ok(); let _ = cx.update_window(window_handle, |_, window, cx| { let _ = this.update(cx, |this, cx| { + let action = this.pending_close_action.take(); this.close_prompt_open = false; - match selection { - Some(0) => this.save(true, window, cx), - Some(1) => window.remove_window(), - _ => { - this.close_after_save = false; - } + match (selection, action) { + (Some(0), Some(action)) => this.save_close_action(action, window, cx), + (Some(1), Some(action)) => this.discard_close_action(action, window, cx), + _ => {} } }); }); @@ -439,7 +753,7 @@ impl RemoteFileEditorWindow { } fn trigger_replace(&mut self, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor.as_ref() else { + let Some(editor) = self.active_tab().and_then(|tab| tab.editor.as_ref()) else { return; }; @@ -449,7 +763,7 @@ impl RemoteFileEditorWindow { } fn focus_editor(&mut self, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor.as_ref() else { + let Some(editor) = self.active_tab().and_then(|tab| tab.editor.as_ref()) else { return; }; @@ -459,25 +773,92 @@ impl RemoteFileEditorWindow { } fn toggle_soft_wrap(&mut self, window: &mut Window, cx: &mut Context) { - self.soft_wrap = !self.soft_wrap; - if let Some(editor) = self.editor.as_ref() { + let Some(tab) = self.active_tab_mut() else { + return; + }; + tab.soft_wrap = !tab.soft_wrap; + if let Some(editor) = tab.editor.as_ref() { editor.update(cx, |state, cx| { - state.set_soft_wrap(self.soft_wrap, window, cx); + state.set_soft_wrap(tab.soft_wrap, window, cx); }); } cx.notify(); } - fn is_dirty(&self, cx: &App) -> bool { - self.editor - .as_ref() - .map(|editor| editor.read(cx).text().to_string() != self.saved_text) + fn switch_tab(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if index >= self.tabs.len() || index == self.active_tab { + return; + } + + self.active_tab = index; + self.update_window_title(window); + self.focus_editor(window, cx); + cx.notify(); + } + + fn is_tab_dirty(&self, index: usize, cx: &App) -> bool { + self.tabs + .get(index) + .map(|tab| tab.is_dirty(cx)) .unwrap_or(false) } + fn has_dirty_tabs(&self, cx: &App) -> bool { + self.tabs.iter().any(|tab| tab.is_dirty(cx)) + } + + fn first_dirty_tab(&self, cx: &App) -> Option { + self.tabs.iter().position(|tab| tab.is_dirty(cx)) + } + + fn render_tabs(&self, cx: &mut Context) -> impl IntoElement { + let mut tab_bar = TabBar::new("remote-file-editor-tabs") + .menu(true) + .with_size(Size::Small) + .selected_index(self.active_tab) + .on_click({ + let view = cx.entity().clone(); + move |index, window, cx| { + let _ = view.update(cx, |this, cx| { + this.switch_tab(*index, window, cx); + }); + } + }); + + for (index, tab) in self.tabs.iter().enumerate() { + let label = if tab.is_dirty(cx) { + format!("* {}", tab.display_name) + } else { + tab.display_name.clone() + }; + tab_bar = tab_bar.child( + Tab::new().label(label).suffix( + Button::new(format!("remote-file-close-tab-{index}")) + .label("×") + .with_size(Size::XSmall) + .disabled(tab.saving) + .on_click(cx.listener(move |this, _, window, cx| { + this.request_close_tab(index, window, cx); + })), + ), + ); + } + + h_flex() + .border_b_1() + .border_color(cx.theme().border) + .bg(cx.theme().tab_bar) + .child(tab_bar) + } + fn render_toolbar(&self, cx: &mut Context) -> impl IntoElement { - let dirty = self.is_dirty(cx); - let disabled = self.loading || self.saving || self.editor.is_none(); + let tab = self.active_tab(); + let dirty = tab.map(|tab| tab.is_dirty(cx)).unwrap_or(false); + let disabled = tab + .map(|tab| tab.loading || tab.saving || tab.editor.is_none()) + .unwrap_or(true); + let loading_or_saving = tab.map(|tab| tab.loading || tab.saving).unwrap_or(true); + let soft_wrap = tab.map(|tab| tab.soft_wrap).unwrap_or(false); h_flex() .gap_2() @@ -518,7 +899,7 @@ impl RemoteFileEditorWindow { Button::new("remote-file-reload") .label(t!("RemoteFileEditor.action.reload")) .with_size(Size::Small) - .disabled(self.loading || self.saving) + .disabled(loading_or_saving) .on_click(cx.listener(|this, _, window, cx| { this.reload(window, cx); })), @@ -526,19 +907,28 @@ impl RemoteFileEditorWindow { .child( Button::new("remote-file-soft-wrap") .label(t!("RemoteFileEditor.action.soft_wrap")) - .selected(self.soft_wrap) + .selected(soft_wrap) .with_size(Size::Small) .disabled(disabled) .on_click(cx.listener(|this, _, window, cx| { this.toggle_soft_wrap(window, cx); })), ) + .child( + Button::new("remote-file-close-active-tab") + .label(t!("RemoteFileEditor.action.close_tab")) + .with_size(Size::Small) + .disabled(loading_or_saving || self.tabs.is_empty()) + .on_click(cx.listener(|this, _, window, cx| { + this.request_close_active_tab(window, cx); + })), + ) .child(div().flex_1()) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) - .child(self.policy_label()), + .child(tab.map(RemoteEditorTab::policy_label).unwrap_or_default()), ) .child( div() @@ -557,6 +947,19 @@ impl RemoteFileEditorWindow { } fn render_status_bar(&self, cx: &mut Context) -> impl IntoElement { + let remote_path = self + .active_tab() + .map(|tab| tab.remote_path.clone()) + .unwrap_or_default(); + let file_size = self + .active_tab() + .map(|tab| tab.file_size) + .unwrap_or_default(); + let status_message = self + .active_tab() + .map(|tab| tab.status_message.clone()) + .unwrap_or_default(); + h_flex() .gap_2() .items_center() @@ -569,25 +972,29 @@ impl RemoteFileEditorWindow { div() .text_sm() .text_color(cx.theme().muted_foreground) - .child(self.remote_path.clone()), + .child(remote_path), ) .child(div().flex_1()) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) - .child(format_size(self.file_size)), + .child(format_size(file_size)), ) .child( div() .text_sm() .text_color(cx.theme().muted_foreground) - .child(self.status_message.clone()), + .child(status_message), ) } fn render_body(&self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.loading { + let Some(tab) = self.active_tab() else { + return v_flex().size_full().into_any_element(); + }; + + if tab.loading { return v_flex() .size_full() .items_center() @@ -596,7 +1003,7 @@ impl RemoteFileEditorWindow { .into_any_element(); } - if let Some(error) = self.load_error.as_ref() { + if let Some(error) = tab.load_error.as_ref() { return v_flex() .size_full() .items_center() @@ -617,7 +1024,7 @@ impl RemoteFileEditorWindow { .into_any_element(); } - match self.editor.as_ref() { + match tab.editor.as_ref() { Some(editor) => v_flex() .size_full() .child(Input::new(editor).size_full()) @@ -625,17 +1032,15 @@ impl RemoteFileEditorWindow { None => v_flex().size_full().into_any_element(), } } - - fn policy_label(&self) -> String { - match self.policy.mode { - EditorMode::Code => t!("RemoteFileEditor.policy.code").to_string(), - EditorMode::PlainText => t!("RemoteFileEditor.policy.plain_text").to_string(), - } - } } impl Render for RemoteFileEditorWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let title = self + .active_tab() + .map(|tab| tab.display_name.clone()) + .unwrap_or_default(); + v_flex() .size_full() .key_context(REMOTE_FILE_EDITOR_CONTEXT) @@ -650,9 +1055,10 @@ impl Render for RemoteFileEditorWindow { .justify_center() .flex_1() .text_sm() - .child(self.display_name.clone()), + .child(title), ), ) + .child(self.render_tabs(cx)) .child(self.render_toolbar(cx)) .child(v_flex().flex_1().child(self.render_body(window, cx))) .child(self.render_status_bar(cx)) @@ -698,4 +1104,16 @@ mod tests { assert_eq!(search_shortcut(), EXPECTED_SEARCH_SHORTCUT); assert_eq!(replace_shortcut(), EXPECTED_REPLACE_SHORTCUT); } + + #[test] + fn display_name_ignores_trailing_slash() { + assert_eq!(display_name_from_path("/tmp/example/"), "example"); + } + + #[test] + fn format_size_uses_binary_units() { + assert_eq!(format_size(42), "42 B"); + assert_eq!(format_size(1024), "1.0 KiB"); + assert_eq!(format_size(1024 * 1024), "1.0 MiB"); + } } diff --git a/crates/remote_file_editor/src/lib.rs b/crates/remote_file_editor/src/lib.rs index e84ca7edf9..83c5b36367 100644 --- a/crates/remote_file_editor/src/lib.rs +++ b/crates/remote_file_editor/src/lib.rs @@ -9,7 +9,10 @@ mod language; #[cfg(feature = "ui")] pub use editor_window::open_remote_file_editor; -pub use close_guard::{CloseIntercept, decide_close_intercept}; +pub use close_guard::{ + CloseIntercept, active_index_after_close, active_index_after_open, decide_close_intercept, + find_tab_index, has_dirty_tabs, +}; pub use file_policy::{ EditorMode, FilePolicy, LARGE_FILE_PLAIN_TEXT_THRESHOLD, MAX_EDITABLE_FILE_SIZE, decode_text_content, determine_file_policy, diff --git a/crates/sftp/src/russh_impl.rs b/crates/sftp/src/russh_impl.rs index 4107e4045e..257dddb6d4 100644 --- a/crates/sftp/src/russh_impl.rs +++ b/crates/sftp/src/russh_impl.rs @@ -1004,7 +1004,7 @@ impl SftpClient for RusshSftpClient { // 按路径深度倒序删除目录(先删子目录) let mut dirs: Vec<&FileEntry> = entries.iter().filter(|e| e.is_dir).collect(); - dirs.sort_by(|a, b| b.path.len().cmp(&a.path.len())); + dirs.sort_by_key(|dir| std::cmp::Reverse(dir.path.len())); for dir in dirs { ensure_not_cancelled(&cancelled)?; progress(TransferProgress { @@ -1201,7 +1201,7 @@ impl SftpClient for RusshSftpClient { .map_err(|e| anyhow!("Failed to create local directory {}: {}", local_path, e))?; let mut dirs: Vec<&FileEntry> = entries.iter().filter(|e| e.is_dir).collect(); - dirs.sort_by(|a, b| a.path.len().cmp(&b.path.len())); + dirs.sort_by_key(|dir| dir.path.len()); for dir_entry in dirs { ensure_not_cancelled(&cancelled)?; let relative = dir_entry @@ -1404,7 +1404,7 @@ impl SftpClient for RusshSftpClient { let _ = self.sftp.create_dir(remote_path).await; let mut dirs: Vec<_> = entries.iter().filter(|(_, is_dir, _)| *is_dir).collect(); - dirs.sort_by(|a, b| a.0.as_os_str().len().cmp(&b.0.as_os_str().len())); + dirs.sort_by_key(|dir| dir.0.as_os_str().len()); for (dir_path, _, _) in dirs { ensure_not_cancelled(&cancelled)?; diff --git a/crates/sftp_view/src/lib.rs b/crates/sftp_view/src/lib.rs index bf0d85b909..6d0e9dae5f 100644 --- a/crates/sftp_view/src/lib.rs +++ b/crates/sftp_view/src/lib.rs @@ -1009,7 +1009,7 @@ impl SftpView { return; }; - open_remote_file_editor(full_path, client, cx); + open_remote_file_editor(full_path, client, window, cx); } fn navigate_local_to(&mut self, path: PathBuf, cx: &mut Context) { diff --git a/crates/ssh/src/ssh.rs b/crates/ssh/src/ssh.rs index 08eee6c536..faff0f9dec 100644 --- a/crates/ssh/src/ssh.rs +++ b/crates/ssh/src/ssh.rs @@ -1,6 +1,5 @@ use std::borrow::Cow; use std::net::SocketAddr; -#[cfg(test)] use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -750,7 +749,6 @@ async fn request_keyboard_interactive_responses( .context(t!("Ssh.auth_keyboard_interactive_cancelled").to_string()) } -#[cfg(test)] pub fn discover_default_private_keys() -> Vec { let Some(home_dir) = dirs::home_dir() else { return Vec::new(); @@ -856,7 +854,6 @@ fn build_auto_publickey_failure_message( parts.join(": ") } -#[cfg(test)] fn path_to_string(path: PathBuf) -> String { path.to_string_lossy().to_string() } @@ -1082,7 +1079,6 @@ mod tests { ); } - #[cfg(unix)] #[test] fn discover_default_private_keys_returns_expected_order() { static ENV_LOCK: OnceLock> = OnceLock::new(); @@ -1129,7 +1125,6 @@ mod tests { ); } - #[cfg(unix)] #[test] fn expand_auto_publickey_auth_contains_agent_and_default_keys() { static ENV_LOCK: OnceLock> = OnceLock::new(); diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 22c9cd831d..e561782f9f 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -52,7 +52,8 @@ actions!( TestAction, Tab, TabPrev, - ShowPanelInfo + ShowPanelInfo, + ToggleListActiveHighlight ] ); diff --git a/crates/story/src/title_bar.rs b/crates/story/src/title_bar.rs index 6e40476408..02fa53cf55 100644 --- a/crates/story/src/title_bar.rs +++ b/crates/story/src/title_bar.rs @@ -14,7 +14,7 @@ use gpui_component::{ scroll::ScrollbarShow, }; -use crate::{SelectFont, SelectRadius, SelectScrollbarShow, app_menus}; +use crate::{SelectFont, SelectRadius, SelectScrollbarShow, ToggleListActiveHighlight, app_menus}; pub struct AppTitleBar { app_menu_bar: Entity, @@ -141,6 +141,17 @@ impl FontSizeSelector { Theme::global_mut(cx).scrollbar_show = show.0; window.refresh(); } + + fn on_toggle_list_active_highlight( + &mut self, + _: &ToggleListActiveHighlight, + window: &mut Window, + cx: &mut Context, + ) { + let theme = Theme::global_mut(cx); + theme.list.active_highlight = !theme.list.active_highlight; + window.refresh(); + } } impl Render for FontSizeSelector { @@ -156,12 +167,13 @@ impl Render for FontSizeSelector { .on_action(cx.listener(Self::on_select_font)) .on_action(cx.listener(Self::on_select_radius)) .on_action(cx.listener(Self::on_select_scrollbar_show)) + .on_action(cx.listener(Self::on_toggle_list_active_highlight)) .child( Button::new("btn") .small() .ghost() .icon(IconName::Settings2) - .dropdown_menu(move |this, _, _cx| { + .dropdown_menu(move |this, _, cx| { this.scrollable(true) .check_side(Side::Right) .max_h(px(480.)) @@ -201,6 +213,11 @@ impl Render for FontSizeSelector { Box::new(SelectScrollbarShow(ScrollbarShow::Always)), ) .separator() + .menu_with_check( + "List Active Highlight", + cx.theme().list.active_highlight, + Box::new(ToggleListActiveHighlight), + ) }) .anchor(Corner::TopRight), ) diff --git a/crates/terminal/src/pty_backend.rs b/crates/terminal/src/pty_backend.rs index b9c12a9d77..758e1a4c4d 100644 --- a/crates/terminal/src/pty_backend.rs +++ b/crates/terminal/src/pty_backend.rs @@ -3,8 +3,10 @@ use alacritty_terminal::event_loop::{EventLoop, EventLoopSender, Msg}; use alacritty_terminal::sync::FairMutex; use alacritty_terminal::term::{ClipboardType, Term}; use alacritty_terminal::tty::{self, Options as PtyOptions}; +use alacritty_terminal::vte::ansi::{NamedColor, Rgb}; use std::borrow::Cow; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; use std::thread; use std::thread::JoinHandle; use tokio::sync::mpsc::UnboundedSender; @@ -155,6 +157,7 @@ impl PtyWriteBack { /// 3. Sends Wakeup event via EventListener pub struct LocalPtyBackend { event_loop_sender: EventLoopSender, + event_proxy: GpuiEventProxy, _event_loop_handle: JoinHandle<()>, child_pid: Option, } @@ -190,6 +193,7 @@ impl LocalPtyBackend { // 设置 PtyWrite 回写通道,使 DA 等终端响应能写回 PTY event_proxy.set_write_back(PtyWriteBack::Local(event_loop_sender.clone())); + event_proxy.set_window_size(window_size); let handle = thread::spawn(move || { let _ = event_loop.spawn().join(); @@ -197,6 +201,7 @@ impl LocalPtyBackend { Ok(Self { event_loop_sender, + event_proxy, _event_loop_handle: handle, child_pid, }) @@ -234,6 +239,7 @@ impl LocalPtyBackend { size.pixel_width, size.pixel_height ); + self.event_proxy.set_window_size(window_size); let _ = self.event_loop_sender.send(Msg::Resize(window_size)); } @@ -248,21 +254,7 @@ impl TerminalBackend for LocalPtyBackend { } fn resize(&self, size: TerminalSize) { - let window_size = WindowSize { - num_lines: size.rows, - num_cols: size.cols, - cell_width: if size.cols > 0 { - size.pixel_width / size.cols - } else { - 8 - }, - cell_height: if size.rows > 0 { - size.pixel_height / size.rows - } else { - 18 - }, - }; - let _ = self.event_loop_sender.send(Msg::Resize(window_size)); + LocalPtyBackend::resize(self, size); } fn close(&self, mode: TerminalCloseMode) { @@ -287,14 +279,25 @@ impl TerminalBackend for LocalPtyBackend { pub struct GpuiEventProxy { event_tx: UnboundedSender, /// PtyWrite 回写通道(在后端创建后设置) - write_back: Arc>>, + write_back: Arc>>, + /// 共享窗口尺寸,供 TextAreaSizeRequest 真实回复使用 + window_size: Arc>, + /// Wakeup 去重标记:true 表示已有未消费的 Wakeup 在事件队列里 + wakeup_pending: Arc, } impl GpuiEventProxy { pub fn new(event_tx: UnboundedSender) -> Self { Self { event_tx, - write_back: Arc::new(std::sync::Mutex::new(None)), + write_back: Arc::new(Mutex::new(None)), + window_size: Arc::new(Mutex::new(WindowSize { + num_lines: 24, + num_cols: 80, + cell_width: 8, + cell_height: 18, + })), + wakeup_pending: Arc::new(AtomicBool::new(false)), } } @@ -318,6 +321,26 @@ impl GpuiEventProxy { self.set_write_back(PtyWriteBack::Hosted { sender, session_id }); } + /// 同步当前真实窗口尺寸(含 cell 像素),后续 TextAreaSizeRequest 将以此回复 + pub(crate) fn set_window_size(&self, size: WindowSize) { + *self.window_size.lock().unwrap() = size; + } + + /// 当 UI 已经消费 Wakeup 后调用,允许下一次 Wakeup 入队 + pub fn reset_wakeup_pending(&self) { + self.wakeup_pending.store(false, Ordering::Release); + } + + /// 返回 Wakeup 去重标记的句柄,便于事件聚合任务在转发 Wakeup 后立即 reset, + /// 让下一次 PTY 输出能继续触发 Wakeup + pub fn wakeup_pending_handle(&self) -> Arc { + self.wakeup_pending.clone() + } + + fn current_window_size(&self) -> WindowSize { + *self.window_size.lock().unwrap() + } + fn write_back(&self, data: Vec) { if let Some(wb) = self.write_back.lock().unwrap().as_ref() { wb.write(data); @@ -332,22 +355,23 @@ impl EventListener for GpuiEventProxy { self.write_back(text.into_bytes()); return; } - AlacTermEvent::ColorRequest(_index, format_fn) => { - let text = format_fn(alacritty_terminal::vte::ansi::Rgb { r: 0, g: 0, b: 0 }); + AlacTermEvent::ColorRequest(index, format_fn) => { + let text = format_fn(default_color_for_index(index)); self.write_back(text.into_bytes()); return; } AlacTermEvent::TextAreaSizeRequest(format_fn) => { - let text = format_fn(WindowSize { - num_lines: 24, - num_cols: 80, - cell_width: 8, - cell_height: 18, - }); + let text = format_fn(self.current_window_size()); self.write_back(text.into_bytes()); return; } - AlacTermEvent::Wakeup => TerminalEvent::Wakeup, + AlacTermEvent::Wakeup => { + // 去重:已有未消费 Wakeup 时直接丢弃,避免高速输出下事件堆积 + if self.wakeup_pending.swap(true, Ordering::AcqRel) { + return; + } + TerminalEvent::Wakeup + } AlacTermEvent::Title(title) => { // 尝试从标题中提取工作目录 // PowerShell 格式: "PS C:\path\to\dir" 或 "PS ~/path" @@ -366,3 +390,120 @@ impl EventListener for GpuiEventProxy { let _ = self.event_tx.send(terminal_event); } } + +/// 为 OSC 4/10/11 等颜色查询提供合理的默认回复,避免一律返回黑色 +fn default_color_for_index(index: usize) -> Rgb { + match index { + // OSC 10:默认前景色 -> 接近白色 + idx if idx == NamedColor::Foreground as usize => Rgb { + r: 0xE4, + g: 0xE4, + b: 0xE4, + }, + // OSC 11:默认背景色 -> 接近深灰 + idx if idx == NamedColor::Background as usize => Rgb { + r: 0x1E, + g: 0x1E, + b: 0x1E, + }, + // OSC 12:光标颜色 + idx if idx == NamedColor::Cursor as usize => Rgb { + r: 0xFF, + g: 0xFF, + b: 0xFF, + }, + _ => Rgb { r: 0, g: 0, b: 0 }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use tokio::sync::mpsc::unbounded_channel; + + #[test] + fn wakeup_dedup_collapses_repeated_wakeups_until_reset() { + let (tx, mut rx) = unbounded_channel::(); + let proxy = GpuiEventProxy::new(tx); + + proxy.send_event(AlacTermEvent::Wakeup); + proxy.send_event(AlacTermEvent::Wakeup); + proxy.send_event(AlacTermEvent::Wakeup); + + // 多次 Wakeup 只入队一次 + let first = rx.try_recv(); + assert!(matches!(first, Ok(TerminalEvent::Wakeup))); + assert!(rx.try_recv().is_err()); + + // reset 后允许新一轮 Wakeup 入队 + proxy.reset_wakeup_pending(); + proxy.send_event(AlacTermEvent::Wakeup); + let next = rx.try_recv(); + assert!(matches!(next, Ok(TerminalEvent::Wakeup))); + } + + #[test] + fn non_wakeup_events_are_not_swallowed_by_dedup() { + let (tx, mut rx) = unbounded_channel::(); + let proxy = GpuiEventProxy::new(tx); + + // 先压一个 Wakeup 进去拉起去重标记 + proxy.send_event(AlacTermEvent::Wakeup); + // 期间发生 Title/Bell/Exit 等事件,不应被去重逻辑吞掉 + proxy.send_event(AlacTermEvent::Title("shell".to_string())); + proxy.send_event(AlacTermEvent::Bell); + proxy.send_event(AlacTermEvent::Exit); + + let mut got = Vec::new(); + while let Ok(ev) = rx.try_recv() { + got.push(ev); + } + assert_eq!(got.len(), 4); + assert!(matches!(got[0], TerminalEvent::Wakeup)); + assert!(matches!(got[1], TerminalEvent::TitleChanged(ref t) if t == "shell")); + assert!(matches!(got[2], TerminalEvent::Bell)); + assert!(matches!(got[3], TerminalEvent::ChildExit(0))); + } + + #[test] + fn text_area_size_request_uses_current_window_size() { + let (tx, _rx) = unbounded_channel::(); + let proxy = GpuiEventProxy::new(tx); + + // 注入一个回写通道收集 reply 字节 + let captured: Arc>> = Arc::new(Mutex::new(Vec::new())); + let (write_tx, mut write_rx) = unbounded_channel::>(); + proxy.set_ssh_write_back(write_tx); + + proxy.set_window_size(WindowSize { + num_lines: 40, + num_cols: 132, + cell_width: 9, + cell_height: 20, + }); + + proxy.send_event(AlacTermEvent::TextAreaSizeRequest(std::sync::Arc::new( + |size| format!("{}x{}", size.num_cols, size.num_lines), + ))); + + if let Ok(bytes) = write_rx.try_recv() { + captured.lock().unwrap().extend_from_slice(&bytes); + } + let reply = String::from_utf8(captured.lock().unwrap().clone()).unwrap(); + assert_eq!(reply, "132x40"); + } + + #[test] + fn color_request_returns_named_defaults_instead_of_black() { + let fg = default_color_for_index(NamedColor::Foreground as usize); + let bg = default_color_for_index(NamedColor::Background as usize); + let cursor = default_color_for_index(NamedColor::Cursor as usize); + let other = default_color_for_index(NamedColor::Red as usize); + + assert_ne!((fg.r, fg.g, fg.b), (0, 0, 0)); + assert_ne!((bg.r, bg.g, bg.b), (0, 0, 0)); + assert_eq!((cursor.r, cursor.g, cursor.b), (0xFF, 0xFF, 0xFF)); + assert_eq!((other.r, other.g, other.b), (0, 0, 0)); + } +} diff --git a/crates/terminal/src/serial_backend.rs b/crates/terminal/src/serial_backend.rs index 813f4a0223..18959fe810 100644 --- a/crates/terminal/src/serial_backend.rs +++ b/crates/terminal/src/serial_backend.rs @@ -26,7 +26,7 @@ impl SerialBackend { params: SerialParams, term: Arc>>, event_tx: UnboundedSender, - on_disconnect: Option>, + on_disconnect: Option>, ) -> anyhow::Result { let data_bits = match params.data_bits { 5 => serialport::DataBits::Five, @@ -99,7 +99,7 @@ impl SerialBackend { } } if let Some(tx) = on_disconnect { - let _ = tx.send(()); + let _ = tx.send(true); } })?; diff --git a/crates/terminal/src/shell_integration.rs b/crates/terminal/src/shell_integration.rs index d16f44a965..0f11ba7fde 100644 --- a/crates/terminal/src/shell_integration.rs +++ b/crates/terminal/src/shell_integration.rs @@ -9,6 +9,8 @@ pub(crate) fn embedded_shell_integration_script() -> String { #[cfg(test)] mod tests { use super::{embedded_shell_integration_script, normalized_shell_integration_script}; + #[cfg(unix)] + use std::{fs, os::unix::fs::PermissionsExt, process::Command}; #[test] fn normalized_shell_integration_script_converts_crlf_to_lf() { @@ -26,4 +28,227 @@ mod tests { "嵌入式 shell integration 脚本不应保留 CR,避免远端 shell 解析失败" ); } + + #[test] + fn embedded_shell_integration_script_enables_vim_mouse() { + let script = embedded_shell_integration_script(); + assert!(script.contains("--cmd 'set mouse=a'")); + assert!(script.contains("--cmd 'nnoremap gkzz'")); + assert!(script.contains("--cmd 'nnoremap gjzz'")); + assert!(script.contains("function vim {")); + assert!(script.contains("__onetcli_can_wrap_command vim")); + } + + #[cfg(unix)] + #[test] + fn bash_vim_wrapper_preserves_args() { + assert_vim_wrapper_preserves_args("bash"); + } + + #[cfg(unix)] + #[test] + fn zsh_vim_wrapper_preserves_args() { + if !shell_available("zsh") { + return; + } + assert_vim_wrapper_preserves_args("zsh"); + } + + #[cfg(unix)] + #[test] + fn bash_vim_wrapper_does_not_override_alias() { + assert_vim_wrapper_does_not_override_alias("bash"); + } + + #[cfg(unix)] + #[test] + fn zsh_vim_wrapper_does_not_override_alias() { + if !shell_available("zsh") { + return; + } + assert_vim_wrapper_does_not_override_alias("zsh"); + } + + #[cfg(unix)] + #[test] + fn bash_vim_wrapper_does_not_override_function() { + assert_vim_wrapper_does_not_override_function("bash"); + } + + #[cfg(unix)] + #[test] + fn zsh_vim_wrapper_does_not_override_function() { + if !shell_available("zsh") { + return; + } + assert_vim_wrapper_does_not_override_function("zsh"); + } + + #[cfg(unix)] + #[test] + fn bash_vim_mouse_can_be_disabled() { + assert_vim_mouse_can_be_disabled("bash"); + } + + #[cfg(unix)] + #[test] + fn zsh_vim_mouse_can_be_disabled() { + if !shell_available("zsh") { + return; + } + assert_vim_mouse_can_be_disabled("zsh"); + } + + #[cfg(unix)] + #[test] + fn bash_nvim_wrapper_preserves_args() { + assert_nvim_wrapper_preserves_args("bash"); + } + + #[cfg(unix)] + #[test] + fn zsh_nvim_wrapper_preserves_args() { + if !shell_available("zsh") { + return; + } + assert_nvim_wrapper_preserves_args("zsh"); + } + + #[cfg(unix)] + fn assert_vim_wrapper_preserves_args(shell: &str) { + let output = run_interactive_shell( + shell, + "source \"$ONETCLI_TEST_SCRIPT\"\nvim 'a b.txt' -- '--weird;$HOME'", + ); + + assert!(output.status.success()); + assert_eq!( + strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)), + "--cmd\nset mouse=a\n--cmd\nnnoremap gkzz\n--cmd\nnnoremap gjzz\n--cmd\ninoremap gkzz\n--cmd\ninoremap gjzz\na b.txt\n--\n--weird;$HOME\n" + ); + } + + #[cfg(unix)] + fn assert_vim_wrapper_does_not_override_alias(shell: &str) { + let output = run_interactive_shell( + shell, + "shopt -s expand_aliases 2>/dev/null || true\nalias vim='echo alias-safe'\nsource \"$ONETCLI_TEST_SCRIPT\"\nvim", + ); + + assert!(output.status.success()); + assert_eq!( + strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)), + "alias-safe\n" + ); + } + + #[cfg(unix)] + fn assert_vim_wrapper_does_not_override_function(shell: &str) { + let output = run_interactive_shell( + shell, + "vim() { echo function-safe; }\nsource \"$ONETCLI_TEST_SCRIPT\"\nvim", + ); + + assert!(output.status.success()); + assert_eq!( + strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)), + "function-safe\n" + ); + } + + #[cfg(unix)] + fn assert_vim_mouse_can_be_disabled(shell: &str) { + let output = run_interactive_shell( + shell, + "source \"$ONETCLI_TEST_SCRIPT\"\nONETCLI_VIM_MOUSE=0 vim file.txt", + ); + + assert!(output.status.success()); + assert_eq!( + strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)), + "file.txt\n" + ); + } + + #[cfg(unix)] + fn assert_nvim_wrapper_preserves_args(shell: &str) { + let output = run_interactive_shell(shell, "source \"$ONETCLI_TEST_SCRIPT\"\nnvim file.txt"); + + assert!(output.status.success()); + assert_eq!( + strip_shell_integration_osc(&String::from_utf8_lossy(&output.stdout)), + "--cmd\nset mouse=a\n--cmd\nnnoremap gkzz\n--cmd\nnnoremap gjzz\n--cmd\ninoremap gkzz\n--cmd\ninoremap gjzz\nfile.txt\n" + ); + } + + #[cfg(unix)] + fn shell_available(shell: &str) -> bool { + let available = Command::new(shell).arg("--version").output().is_ok(); + if !available { + eprintln!("跳过 {shell} 行为测试:当前环境未安装该 shell"); + } + available + } + + #[cfg(unix)] + fn run_interactive_shell(shell: &str, command: &str) -> std::process::Output { + let temp_dir = std::env::temp_dir().join(format!( + "onetcli-shell-integration-test-{}-{:?}", + std::process::id(), + std::thread::current().id() + )); + let bin_dir = temp_dir.join("bin"); + let home_dir = temp_dir.join("home"); + let zdot_dir = temp_dir.join("zsh"); + fs::create_dir_all(&bin_dir).expect("应创建测试 bin 目录"); + fs::create_dir_all(&home_dir).expect("应创建测试 HOME 目录"); + fs::create_dir_all(&zdot_dir).expect("应创建测试 ZDOTDIR 目录"); + + let script_path = temp_dir.join("shell_integration.sh"); + fs::write(&script_path, embedded_shell_integration_script()).expect("应写入集成脚本"); + + write_fake_editor(&bin_dir.join("vim")); + write_fake_editor(&bin_dir.join("nvim")); + + let command_path = temp_dir.join("command.sh"); + fs::write(&command_path, command).expect("应写入测试命令脚本"); + + let path = format!( + "{}:{}", + bin_dir.display(), + std::env::var("PATH").unwrap_or_default() + ); + let output = Command::new(shell) + .arg("-i") + .arg(&command_path) + .env("PATH", path) + .env("HOME", &home_dir) + .env("ZDOTDIR", &zdot_dir) + .env("ONETCLI_TEST_SCRIPT", &script_path) + .output() + .expect("应执行 shell 行为测试"); + + let _ = fs::remove_dir_all(&temp_dir); + output + } + + #[cfg(unix)] + fn write_fake_editor(path: &std::path::Path) { + fs::write( + path, + "#!/bin/sh\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done\n", + ) + .expect("应写入 fake editor"); + fs::set_permissions(path, fs::Permissions::from_mode(0o755)) + .expect("应设置 fake editor 可执行权限"); + } + + #[cfg(unix)] + fn strip_shell_integration_osc(output: &str) -> String { + output + .replace("\u{1b}]133;C\u{7}", "") + .replace("\u{1b}]133;D;0\u{7}", "") + .replace("\u{1b}]133;A\u{7}", "") + .replace("\u{1b}]133;B\u{7}", "") + } } diff --git a/crates/terminal/src/shell_integration.sh b/crates/terminal/src/shell_integration.sh index dadb622bf2..2559bf1738 100644 --- a/crates/terminal/src/shell_integration.sh +++ b/crates/terminal/src/shell_integration.sh @@ -50,6 +50,45 @@ __onetcli_last_history_command() { fi } +__onetcli_enable_vim_mouse() { + local editor="$1" + shift + if [[ "${ONETCLI_VIM_MOUSE:-1}" == "0" ]]; then + command "$editor" "$@" + return + fi + command "$editor" \ + --cmd 'set mouse=a' \ + --cmd 'nnoremap gkzz' \ + --cmd 'nnoremap gjzz' \ + --cmd 'inoremap gkzz' \ + --cmd 'inoremap gjzz' \ + "$@" +} + +__onetcli_can_wrap_command() { + local name="$1" + if [[ -n "${ZSH_VERSION:-}" ]]; then + local command_type + command_type="$(whence -w "$name" 2>/dev/null)" + [[ "$command_type" == "$name: command" || "$command_type" == "$name: hashed" ]] + else + [[ "$(type -t "$name" 2>/dev/null)" == "file" ]] + fi +} + +if __onetcli_can_wrap_command vim; then + function vim { + __onetcli_enable_vim_mouse vim "$@" + } +fi + +if __onetcli_can_wrap_command nvim; then + function nvim { + __onetcli_enable_vim_mouse nvim "$@" + } +fi + __onetcli_emit_recorded_command() { local command_text encoded command_text="$(__onetcli_last_history_command)" diff --git a/crates/terminal/src/ssh_backend.rs b/crates/terminal/src/ssh_backend.rs index e74e483b92..136034b170 100644 --- a/crates/terminal/src/ssh_backend.rs +++ b/crates/terminal/src/ssh_backend.rs @@ -6,10 +6,8 @@ use alacritty_terminal::sync::FairMutex; use alacritty_terminal::term::Term; use alacritty_terminal::vte::ansi::{Processor, StdSyncHandler}; -use crate::TerminalCloseMode; use ssh::{ - ChannelEvent, PtyConfig, ShellIntegrationSetup, SshChannel, SshClient, SshConnectionStage, - SshSessionManager, + ChannelEvent, PtyConfig, ShellIntegrationSetup, SshChannel, SshClient, SshSessionManager, }; use crate::osc::{OscEvent, extract_osc_events}; @@ -17,7 +15,7 @@ use crate::pty_backend::{GpuiEventProxy, TerminalEvent}; use crate::shell_integration::{ embedded_shell_integration_script, normalized_shell_integration_script, }; -use crate::{TerminalBackend, TerminalSize}; +use crate::{TerminalBackend, TerminalCloseMode, TerminalSize}; /// 整个 shell integration 安装流程的硬超时,避免远端受限或挂死卡住连接。 const SHELL_INTEGRATION_SETUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -90,22 +88,43 @@ fn build_shell_integration_setup_script( let home_marker = shell_single_quote(home_marker); let session_marker = shell_single_quote(session_marker); let shell_marker = shell_single_quote(shell_marker); + + // zsh wrapper 设计:让 ZDOTDIR 始终保持 session_dir/zsh,在该目录下放完整的 4 个 wrapper + // 文件,每个 fan-out 到 $ONETCLI_ORIG_ZDOTDIR 下的同名文件,保留完整 login shell 行为; + // 仅在 .zshrc 末尾追加 integration source,然后还原 ZDOTDIR 给后续 sub-shell。 let zshenv = shell_single_quote( - "_ONETCLI_SESSION_ZDOTDIR=\"$ZDOTDIR\"\n\ - _ONETCLI_ORIG_ZDOTDIR=\"${ONETCLI_ORIG_ZDOTDIR:-$HOME}\"\n\ - [[ -f \"$_ONETCLI_ORIG_ZDOTDIR/.zshenv\" ]] && . \"$_ONETCLI_ORIG_ZDOTDIR/.zshenv\"\n\ - ZDOTDIR=\"$_ONETCLI_SESSION_ZDOTDIR\"\n\ - export ZDOTDIR\n\ - unset _ONETCLI_SESSION_ZDOTDIR _ONETCLI_ORIG_ZDOTDIR\n", + "[[ -n \"${ONETCLI_ORIG_ZDOTDIR:-}\" ]] && [ -f \"$ONETCLI_ORIG_ZDOTDIR/.zshenv\" ] \ + && . \"$ONETCLI_ORIG_ZDOTDIR/.zshenv\"\n", + ); + let zprofile = shell_single_quote( + "[[ -n \"${ONETCLI_ORIG_ZDOTDIR:-}\" ]] && [ -f \"$ONETCLI_ORIG_ZDOTDIR/.zprofile\" ] \ + && . \"$ONETCLI_ORIG_ZDOTDIR/.zprofile\"\n", ); let zshrc = shell_single_quote(&format!( - "_ONETCLI_ORIG_ZDOTDIR=\"${{ONETCLI_ORIG_ZDOTDIR:-$HOME}}\"\n\ - [[ -f \"$_ONETCLI_ORIG_ZDOTDIR/.zshrc\" ]] && . \"$_ONETCLI_ORIG_ZDOTDIR/.zshrc\"\n\ - . \"{integration_source}\"\n" + "[[ -n \"${{ONETCLI_ORIG_ZDOTDIR:-}}\" ]] && [ -f \"$ONETCLI_ORIG_ZDOTDIR/.zshrc\" ] \ + && . \"$ONETCLI_ORIG_ZDOTDIR/.zshrc\"\n\ + . \"{integration_source}\"\n\ + ZDOTDIR=\"${{ONETCLI_ORIG_ZDOTDIR:-$HOME}}\"\n" )); + let zlogin = shell_single_quote( + "[[ -n \"${ONETCLI_ORIG_ZDOTDIR:-}\" ]] && [ -f \"$ONETCLI_ORIG_ZDOTDIR/.zlogin\" ] \ + && . \"$ONETCLI_ORIG_ZDOTDIR/.zlogin\"\n", + ); + // bash wrapper:`exec bash --rcfile X -i` 是 interactive non-login,跳过 /etc/profile 与 + // ~/.bash_profile 等。这里手动模拟 login chain,然后再显式 source ~/.bashrc + integration。 + // ONETCLI_LOGIN_SIMULATED guard 防止 .bash_profile 内 `exec bash -l` 等场景二次进入时重复 + // 加载 profile 链。 let bashrc = shell_single_quote(&format!( - "[ -f \"$HOME/.bashrc\" ] && . \"$HOME/.bashrc\"\n\ - . \"{integration_source}\"\n" + "if [ -z \"${{ONETCLI_LOGIN_SIMULATED:-}}\" ]; then\n\ + \x20\x20\x20\x20export ONETCLI_LOGIN_SIMULATED=1\n\ + \x20\x20\x20\x20[ -r /etc/profile ] && . /etc/profile\n\ + \x20\x20\x20\x20for __onetcli_profile in \"$HOME/.bash_profile\" \"$HOME/.bash_login\" \"$HOME/.profile\"; do\n\ + \x20\x20\x20\x20\x20\x20\x20\x20if [ -r \"$__onetcli_profile\" ]; then . \"$__onetcli_profile\"; break; fi\n\ + \x20\x20\x20\x20done\n\ + \x20\x20\x20\x20unset __onetcli_profile\n\ + fi\n\ + [ -r \"$HOME/.bashrc\" ] && . \"$HOME/.bashrc\"\n\ + . \"{integration_source}\"\n" )); format!( @@ -118,7 +137,9 @@ fn build_shell_integration_setup_script( "mkdir -p \"$zsh_dir\" \"$bash_dir\"\n", "printf %s {script} > \"$integration_path\"\n", "printf %s {zshenv} > \"$zsh_dir/.zshenv\"\n", + "printf %s {zprofile} > \"$zsh_dir/.zprofile\"\n", "printf %s {zshrc} > \"$zsh_dir/.zshrc\"\n", + "printf %s {zlogin} > \"$zsh_dir/.zlogin\"\n", "printf %s {bashrc} > \"$bash_dir/.bashrc\"\n", "printf '%s%s\\n' {home_marker} \"$HOME\"\n", "printf '%s%s\\n' {session_marker} \"$session_dir\"\n", @@ -128,7 +149,9 @@ fn build_shell_integration_setup_script( session_key = session_key, script = script, zshenv = zshenv, + zprofile = zprofile, zshrc = zshrc, + zlogin = zlogin, bashrc = bashrc, success_marker = success_marker, home_marker = home_marker, @@ -157,18 +180,6 @@ fn format_setup_failure_context(script: &str, stdout: &[u8], stderr: &[u8]) -> S ) } -fn build_zsh_interactive_command(setup: &ShellIntegrationSetup) -> String { - let shell_path = setup.login_shell.as_deref().unwrap_or("zsh"); - let zsh_dir = format!("{}/zsh", setup.session_dir); - - format!( - "exec env ONETCLI_SHELL_INTEGRATION=1 ONETCLI_ORIG_ZDOTDIR={} ZDOTDIR={} {} -i", - shell_single_quote(&setup.home_dir), - shell_single_quote(&zsh_dir), - shell_single_quote(shell_path) - ) -} - enum SshCommand { Write(Vec), Resize(TerminalSize), @@ -190,11 +201,16 @@ impl SshBackend { notify_tx: UnboundedSender<()>, on_disconnect: Option>, init_commands: Option, + disable_shell_integration: bool, ) -> anyhow::Result { - let (client, mut channel) = - Self::establish_channel(&session_manager, &pty_config, connection_id) - .await - .map_err(add_connect_error_context)?; + let (client, mut channel) = Self::establish_channel( + &session_manager, + &pty_config, + connection_id, + disable_shell_integration, + ) + .await + .map_err(add_connect_error_context)?; // 关联变量,避免 clippy 警告未使用。 let _keep_client = client; @@ -261,9 +277,6 @@ impl SshBackend { OscEvent::WorkingDirChanged(path) => { let _ = event_tx.send(TerminalEvent::WorkingDirChanged(path)); } - OscEvent::SshPromptReady => { - let _ = event_tx.send(TerminalEvent::SshPromptReady); - } OscEvent::PromptStart => { let _ = event_tx.send(TerminalEvent::PromptStart); } @@ -289,147 +302,13 @@ impl SshBackend { TerminalEvent::CommandRecorded(command) ); } - } - } - - // shell ready 后发送 init_commands(只发一次) - if shell_ready && !init_sent { - init_sent = true; - if let Some(ref commands) = pending_init { - for line in commands.lines() { - if !line.trim().is_empty() { - let mut cmd_data = line.as_bytes().to_vec(); - cmd_data.push(b'\n'); - let _ = channel.send_data(&cmd_data).await; - } - } - } - } - - processor.advance(&mut *term.lock(), &data); - let _ = notify_tx.send(()); - } - Some(ChannelEvent::Eof) | Some(ChannelEvent::Close) | None => { - break; - } - _ => {} - } - } - } - } - - if !shutdown { - let _ = session_manager.invalidate().await; - } - if let Some(tx) = on_disconnect { - let _ = tx.send(()); - } - }); - - Ok(Self { command_tx }) - } - - pub async fn connect_with_progress( - session_manager: Arc, - pty_config: PtyConfig, - connection_id: Option, - term: Arc>>, - event_proxy: GpuiEventProxy, - event_tx: UnboundedSender, - notify_tx: UnboundedSender<()>, - on_disconnect: Option>, - init_commands: Option, - _on_progress: impl FnMut(SshConnectionStage) + Send + 'static, - ) -> anyhow::Result { - let (client, mut channel) = - Self::establish_channel(&session_manager, &pty_config, connection_id) - .await - .map_err(add_connect_error_context)?; - let _keep_client = client; - - let pending_init = init_commands; - let (command_tx, mut command_rx) = unbounded_channel::(); - let (pty_write_tx, mut pty_write_rx) = unbounded_channel::>(); - event_proxy.set_ssh_write_back(pty_write_tx); - - tokio::spawn(async move { - let mut shutdown = false; - let mut processor: Processor = Processor::new(); - let mut shell_ready = false; - let mut init_sent = false; - - loop { - tokio::select! { - biased; - Some(cmd) = command_rx.recv() => { - match cmd { - SshCommand::Write(data) => { - let send_result = tokio::time::timeout( - Duration::from_secs(30), - channel.send_data(&data) - ).await; - if send_result.is_err() || send_result.is_ok_and(|r| r.is_err()) { - break; - } - } - SshCommand::Resize(size) => { - let _ = channel.resize_pty(size.cols as u32, size.rows as u32).await; - } - SshCommand::Shutdown => { - shutdown = true; - let _ = channel.close().await; - break; - } - } - } - Some(data) = pty_write_rx.recv() => { - let send_result = tokio::time::timeout( - Duration::from_secs(30), - channel.send_data(&data) - ).await; - if send_result.is_err() || send_result.is_ok_and(|r| r.is_err()) { - break; - } - } - event = channel.recv() => { - match event { - Some(ChannelEvent::Data(data)) | Some(ChannelEvent::ExtendedData { data, .. }) => { - for osc_event in extract_osc_events(&data) { - tracing::debug!( - target: "terminal.history_prompt.osc", - event = ?osc_event, - "ssh backend observed osc event" - ); - match osc_event { - OscEvent::WorkingDirChanged(path) => { - let _ = event_tx.send(TerminalEvent::WorkingDirChanged(path)); - } OscEvent::SshPromptReady => { let _ = event_tx.send(TerminalEvent::SshPromptReady); } - OscEvent::PromptStart => { - let _ = event_tx.send(TerminalEvent::PromptStart); - } - OscEvent::InputStart => { - let _ = event_tx.send(TerminalEvent::InputStart); - if !shell_ready { - shell_ready = true; - } - } - OscEvent::CommandStart => {} - OscEvent::CommandFinished { exit_code } => { - let _ = event_tx.send( - TerminalEvent::CommandFinished { exit_code } - ); - } - OscEvent::CommandRecorded(command) => { - let _ = event_tx.send( - TerminalEvent::CommandRecorded(command) - ); - } } } + // shell ready 后发送 init_commands(只发一次) if shell_ready && !init_sent { init_sent = true; if let Some(ref commands) = pending_init { @@ -472,6 +351,7 @@ impl SshBackend { session_manager: &Arc, pty_config: &PtyConfig, connection_id: Option, + disable_shell_integration: bool, ) -> anyhow::Result<(Arc>, ssh::RusshChannel)> { let mut attempt = 0usize; loop { @@ -480,7 +360,14 @@ impl SshBackend { let result = { let mut guard = client.lock().await; - Self::prepare_ssh_channel(&mut *guard, pty_config, connection_id, cached).await + Self::prepare_ssh_channel( + &mut *guard, + pty_config, + connection_id, + cached, + disable_shell_integration, + ) + .await }; match result { @@ -510,8 +397,13 @@ impl SshBackend { pty_config: &PtyConfig, connection_id: Option, cached: Option, + disable_shell_integration: bool, ) -> anyhow::Result<(C::Channel, Option)> { - let (setup, new_setup) = if let Some(cached) = cached { + let (setup, new_setup) = if disable_shell_integration { + // 用户在连接配置里显式关闭了 shell integration:跳过安装,走裸 request_shell 路径, + // 不向 manager 写入任何缓存,确保下次连接如果用户改回开启时还能正常走 setup。 + (None, None) + } else if let Some(cached) = cached { (Some(cached), None) } else { // 首次连接:尝试安装 integration,失败降级为"无 integration"分支。 @@ -685,9 +577,11 @@ impl SshBackend { match setup.login_shell.as_deref().map(shell_basename) { Some("zsh") => { + channel + .set_env("ZDOTDIR", &format!("{}/zsh", setup.session_dir)) + .await?; channel.request_pty(pty_config).await?; - let command = build_zsh_interactive_command(setup); - channel.exec(&command).await?; + channel.request_shell().await?; } Some("bash") => { channel.request_pty(pty_config).await?; @@ -735,7 +629,6 @@ mod tests { #[derive(Default)] struct MockChannelState { ops: Vec, - exec_commands: Vec, events: VecDeque, exec_consumes_session: bool, recv_delay: Option, @@ -760,7 +653,6 @@ mod tests { ) -> (Self, Arc>) { let state = Arc::new(Mutex::new(MockChannelState { ops: Vec::new(), - exec_commands: Vec::new(), events: events.into_iter().collect(), exec_consumes_session, recv_delay, @@ -788,7 +680,6 @@ mod tests { async fn exec(&mut self, _command: &str) -> Result<()> { let mut state = self.state.lock().expect("mock channel state should lock"); state.ops.push(ChannelOp::Exec); - state.exec_commands.push(_command.to_string()); Ok(()) } @@ -897,14 +788,6 @@ mod tests { .clone() } - fn recorded_exec_commands(state: &Arc>) -> Vec { - state - .lock() - .expect("mock channel state should lock") - .exec_commands - .clone() - } - #[tokio::test] async fn prepare_ssh_channel_uses_dedicated_setup_channel_for_zsh() { let (setup_channel, setup_state) = MockChannel::new( @@ -920,9 +803,14 @@ mod tests { let (interactive_channel, interactive_state) = MockChannel::new([], false); let mut client = MockClient::new([setup_channel, interactive_channel]); - let result = - SshBackend::prepare_ssh_channel(&mut client, &PtyConfig::default(), Some(42), None) - .await; + let result = SshBackend::prepare_ssh_channel( + &mut client, + &PtyConfig::default(), + Some(42), + None, + false, + ) + .await; let (_channel, new_setup) = result.expect("安装 shell integration 不应占用交互 shell 的 channel"); @@ -939,29 +827,14 @@ mod tests { vec![ ChannelOp::SetEnv("ONETCLI_SHELL_INTEGRATION".into(), "1".into()), ChannelOp::SetEnv("ONETCLI_ORIG_ZDOTDIR".into(), "/tmp/home".into()), + ChannelOp::SetEnv( + "ZDOTDIR".into(), + "/tmp/home/.config/onetcli/sessions/42/zsh".into(), + ), ChannelOp::RequestPty, - ChannelOp::Exec, + ChannelOp::RequestShell, ] ); - let exec_commands = recorded_exec_commands(&interactive_state); - assert_eq!(exec_commands.len(), 1, "zsh 交互通道应只执行一次启动命令"); - let command = &exec_commands[0]; - assert!( - command.contains("exec env ONETCLI_SHELL_INTEGRATION=1"), - "zsh 应通过显式 env 命令启动集成 shell: {command}" - ); - assert!( - command.contains("ONETCLI_ORIG_ZDOTDIR='/tmp/home'"), - "zsh 启动命令应内联原始 ZDOTDIR: {command}" - ); - assert!( - command.contains("ZDOTDIR='/tmp/home/.config/onetcli/sessions/42/zsh'"), - "zsh 启动命令应内联 session ZDOTDIR: {command}" - ); - assert!( - command.contains("'/bin/zsh' -i"), - "zsh 启动命令应显式执行交互式 zsh: {command}" - ); } #[tokio::test] @@ -979,9 +852,14 @@ mod tests { let (interactive_channel, interactive_state) = MockChannel::new([], false); let mut client = MockClient::new([setup_channel, interactive_channel]); - let result = - SshBackend::prepare_ssh_channel(&mut client, &PtyConfig::default(), Some(42), None) - .await; + let result = SshBackend::prepare_ssh_channel( + &mut client, + &PtyConfig::default(), + Some(42), + None, + false, + ) + .await; let (_channel, new_setup) = result.expect("bash shell wrapper 应通过独立交互 channel 启动"); assert!(new_setup.is_some()); @@ -1084,10 +962,15 @@ mod tests { let (interactive_channel, interactive_state) = MockChannel::new([], false); let mut client = MockClient::new([setup_channel, interactive_channel]); - let (_ch, new_setup) = - SshBackend::prepare_ssh_channel(&mut client, &PtyConfig::default(), Some(42), None) - .await - .expect("setup 失败时 prepare_ssh_channel 不应整体失败"); + let (_ch, new_setup) = SshBackend::prepare_ssh_channel( + &mut client, + &PtyConfig::default(), + Some(42), + None, + false, + ) + .await + .expect("setup 失败时 prepare_ssh_channel 不应整体失败"); assert!( new_setup.is_none(), @@ -1122,6 +1005,7 @@ mod tests { &PtyConfig::default(), Some(42), Some(cached), + false, ) .await .expect("缓存命中时应直接复用 setup 结果"); @@ -1145,6 +1029,34 @@ mod tests { ); } + #[tokio::test] + async fn prepare_ssh_channel_skips_setup_when_disabled() { + // 用户在连接配置里显式关闭 shell integration:不开 setup channel,只开 1 个 interactive + // channel 走裸 PTY + shell;且不向 manager 写入任何缓存。 + let (interactive_channel, interactive_state) = MockChannel::new([], false); + let mut client = MockClient::new([interactive_channel]); + + let (_ch, new_setup) = SshBackend::prepare_ssh_channel( + &mut client, + &PtyConfig::default(), + Some(42), + None, + true, + ) + .await + .expect("禁用 shell integration 时仍应建立 interactive channel"); + + assert!( + new_setup.is_none(), + "禁用路径不应向 manager 写入任何 integration 缓存" + ); + assert_eq!( + recorded_ops(&interactive_state), + vec![ChannelOp::RequestPty, ChannelOp::RequestShell], + "禁用路径只跑 pty + shell,不调 set_env / exec wrapper" + ); + } + #[tokio::test] async fn try_install_shell_integration_times_out_in_ten_seconds() { // 测试里用短 timeout 验证逻辑;生产路径仍走 10s 常量。 @@ -1233,49 +1145,59 @@ mod tests { fs::read_to_string(&integration_path).expect("应写入 integration 文件"), script ); - let session_zshenv = - fs::read_to_string(session_dir.join("zsh/.zshenv")).expect("应读取 zshenv wrapper"); - let session_zshrc = - fs::read_to_string(session_dir.join("zsh/.zshrc")).expect("应读取 zshrc wrapper"); assert!( session_dir.join("zsh/.zshenv").is_file(), - "应写入 zsh session wrapper" + "应写入 zsh session wrapper (.zshenv)" + ); + assert!( + session_dir.join("zsh/.zprofile").is_file(), + "应写入 zsh session wrapper (.zprofile)" ); assert!( session_dir.join("zsh/.zshrc").is_file(), "应写入 zshrc session wrapper" ); + assert!( + session_dir.join("zsh/.zlogin").is_file(), + "应写入 zsh session wrapper (.zlogin)" + ); assert!( session_dir.join("bash/.bashrc").is_file(), "应写入 bash session wrapper" ); - assert_eq!( - fs::read_to_string(&bashrc_path).expect("应保留用户 bashrc"), - "# user bashrc\n" - ); - assert_eq!( - fs::read_to_string(&zshrc_path).expect("应保留用户 zshrc"), - "# user zshrc\n" - ); + + let zshrc_wrapper = + fs::read_to_string(session_dir.join("zsh/.zshrc")).expect("应读取 zshrc wrapper"); assert!( - session_zshenv.contains("_ONETCLI_SESSION_ZDOTDIR=\"$ZDOTDIR\""), - "zshenv wrapper 应保留 session ZDOTDIR" + zshrc_wrapper.contains("shell_integration.sh"), + ".zshrc wrapper 应在末尾 source integration: {zshrc_wrapper}" ); assert!( - session_zshenv.contains("$_ONETCLI_ORIG_ZDOTDIR/.zshenv"), - "zshenv wrapper 应按原始目录加载用户 zshenv" + zshrc_wrapper.contains("ZDOTDIR=\"${ONETCLI_ORIG_ZDOTDIR:-$HOME}\""), + ".zshrc wrapper 应在末尾还原 ZDOTDIR: {zshrc_wrapper}" ); + + let bashrc_wrapper = + fs::read_to_string(session_dir.join("bash/.bashrc")).expect("应读取 bashrc wrapper"); assert!( - session_zshenv.contains("ZDOTDIR=\"$_ONETCLI_SESSION_ZDOTDIR\""), - "zshenv wrapper 应恢复 session ZDOTDIR" + bashrc_wrapper.contains("ONETCLI_LOGIN_SIMULATED"), + ".bashrc wrapper 应包含 ONETCLI_LOGIN_SIMULATED guard 模拟 login chain: {bashrc_wrapper}" ); assert!( - session_zshrc.contains("$_ONETCLI_ORIG_ZDOTDIR/.zshrc"), - "zshrc wrapper 应按原始目录加载用户 zshrc" + bashrc_wrapper.contains("/etc/profile"), + ".bashrc wrapper 应模拟 login shell 加载 /etc/profile: {bashrc_wrapper}" ); assert!( - session_zshrc.contains(". \"$HOME/.config/onetcli/sessions/42/shell_integration.sh\""), - "zshrc wrapper 应加载 onetcli shell integration" + bashrc_wrapper.contains(".bash_profile"), + ".bashrc wrapper 应模拟 login shell 尝试 ~/.bash_profile: {bashrc_wrapper}" + ); + assert_eq!( + fs::read_to_string(&bashrc_path).expect("应保留用户 bashrc"), + "# user bashrc\n" + ); + assert_eq!( + fs::read_to_string(&zshrc_path).expect("应保留用户 zshrc"), + "# user zshrc\n" ); assert_eq!( String::from_utf8_lossy(&output.stdout).trim(), @@ -1336,6 +1258,189 @@ mod tests { let _ = fs::remove_dir_all(&temp_dir); } + #[cfg(unix)] + #[test] + fn bash_wrapper_runs_bash_profile_chain_and_integration() { + if Command::new("bash").arg("--version").output().is_err() { + eprintln!("跳过 bash wrapper 测试:当前环境未安装 bash"); + return; + } + let temp_dir = std::env::temp_dir().join(format!( + "onetcli-bash-wrapper-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time should be after unix epoch") + .as_nanos() + )); + fs::create_dir_all(&temp_dir).expect("应创建临时目录"); + + let home_dir = temp_dir.join("home"); + fs::create_dir_all(&home_dir).expect("应创建 home 目录"); + fs::write( + home_dir.join(".bash_profile"), + "export __ONETCLI_BASH_PROFILE_LOADED=1\n", + ) + .expect("应写入用户 .bash_profile"); + fs::write( + home_dir.join(".bashrc"), + "[[ $- != *i* ]] && return\nexport __ONETCLI_USER_BASHRC=1\n", + ) + .expect("应写入用户 .bashrc"); + + let script = "export __ONETCLI_INTEGRATION_LOADED=1\n"; + let command = build_shell_integration_setup_script( + script, + "42", + "__TEST_OK__", + "__HOME__=", + "__SESSION__=", + "__SHELL__=", + ); + let setup = Command::new("sh") + .arg("-c") + .arg(&command) + .env("HOME", &home_dir) + .output() + .expect("应执行 setup 脚本"); + assert!( + setup.status.success(), + "setup 脚本应成功: {}", + String::from_utf8_lossy(&setup.stderr) + ); + + let wrapper = home_dir.join(".config/onetcli/sessions/42/bash/.bashrc"); + let output = Command::new("bash") + .arg("--rcfile") + .arg(&wrapper) + .arg("-i") + .arg("-c") + .arg( + "echo profile=$__ONETCLI_BASH_PROFILE_LOADED \ + rc=$__ONETCLI_USER_BASHRC \ + integration=$__ONETCLI_INTEGRATION_LOADED \ + login=$ONETCLI_LOGIN_SIMULATED", + ) + .env("HOME", &home_dir) + .env("PS1", "$ ") + .output() + .expect("应执行 bash wrapper"); + + assert!( + output.status.success(), + "bash wrapper 应成功执行: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("profile=1"), + "bash wrapper 应模拟 login shell 加载 .bash_profile,实际: {stdout}" + ); + assert!( + stdout.contains("rc=1"), + "bash wrapper 应显式 source 用户 .bashrc,实际: {stdout}" + ); + assert!( + stdout.contains("integration=1"), + "bash wrapper 应在末尾 source shell integration,实际: {stdout}" + ); + assert!( + stdout.contains("login=1"), + "bash wrapper 应设置 ONETCLI_LOGIN_SIMULATED guard,实际: {stdout}" + ); + + let _ = fs::remove_dir_all(&temp_dir); + } + + #[cfg(unix)] + #[test] + fn zsh_wrapper_loads_user_files_integration_and_restores_zdotdir() { + if Command::new("zsh").arg("--version").output().is_err() { + eprintln!("跳过 zsh wrapper 测试:当前环境未安装 zsh"); + return; + } + let temp_dir = std::env::temp_dir().join(format!( + "onetcli-zsh-wrapper-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time should be after unix epoch") + .as_nanos() + )); + fs::create_dir_all(&temp_dir).expect("应创建临时目录"); + + let home_dir = temp_dir.join("home"); + fs::create_dir_all(&home_dir).expect("应创建 home 目录"); + fs::write(home_dir.join(".zshenv"), "export __ONETCLI_USER_ZSHENV=1\n") + .expect("应写入用户 .zshenv"); + fs::write(home_dir.join(".zshrc"), "export __ONETCLI_USER_ZSHRC=1\n") + .expect("应写入用户 .zshrc"); + + let script = "export __ONETCLI_INTEGRATION_LOADED=1\n"; + let command = build_shell_integration_setup_script( + script, + "42", + "__TEST_OK__", + "__HOME__=", + "__SESSION__=", + "__SHELL__=", + ); + let setup = Command::new("sh") + .arg("-c") + .arg(&command) + .env("HOME", &home_dir) + .output() + .expect("应执行 setup 脚本"); + assert!( + setup.status.success(), + "setup 脚本应成功: {}", + String::from_utf8_lossy(&setup.stderr) + ); + + let zsh_dir = home_dir.join(".config/onetcli/sessions/42/zsh"); + let output = Command::new("zsh") + .arg("-i") + .arg("-c") + .arg( + "echo zshenv=$__ONETCLI_USER_ZSHENV \ + zshrc=$__ONETCLI_USER_ZSHRC \ + integration=$__ONETCLI_INTEGRATION_LOADED \ + zdotdir=$ZDOTDIR", + ) + .env("HOME", &home_dir) + .env("ZDOTDIR", &zsh_dir) + .env("ONETCLI_ORIG_ZDOTDIR", &home_dir) + .output() + .expect("应执行 zsh wrapper"); + + assert!( + output.status.success(), + "zsh wrapper 应成功执行: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("zshenv=1"), + "zsh wrapper 应通过 fan-out 加载用户 .zshenv,实际: {stdout}" + ); + assert!( + stdout.contains("zshrc=1"), + "zsh wrapper 应通过 fan-out 加载用户 .zshrc,实际: {stdout}" + ); + assert!( + stdout.contains("integration=1"), + "zsh wrapper 应在 .zshrc 末尾 source shell integration,实际: {stdout}" + ); + assert!( + stdout.contains(&format!("zdotdir={}", home_dir.display())), + "zsh wrapper 应在 .zshrc 末尾把 ZDOTDIR 还原为 $HOME,实际: {stdout}" + ); + + let _ = fs::remove_dir_all(&temp_dir); + } + #[test] fn parse_osc_payload_decodes_recorded_command() { let payload = "1337;Command=Z2l0IHN0YXR1cw=="; @@ -1381,11 +1486,11 @@ impl TerminalBackend for SshBackend { let _ = self.command_tx.send(SshCommand::Resize(size)); } - fn shutdown(&self) { + fn close(&self, _mode: TerminalCloseMode) { let _ = self.command_tx.send(SshCommand::Shutdown); } - fn close(&self, _mode: TerminalCloseMode) { + fn shutdown(&self) { let _ = self.command_tx.send(SshCommand::Shutdown); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a0a4adec91..830970ae08 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -15,7 +15,8 @@ use alacritty_terminal::term::cell::{Flags, LineLength}; use alacritty_terminal::term::{Config as TermConfig, Term, TermMode}; use alacritty_terminal::tty::{self, Options as PtyOptions}; use alacritty_terminal::vte::ansi::{Processor, StdSyncHandler}; -use anyhow::Result; +use anyhow::{Result, anyhow}; +use async_trait::async_trait; use futures::StreamExt; use gpui::*; use one_core::gpui_tokio::Tokio; @@ -30,9 +31,11 @@ use std::collections::HashSet; use std::collections::VecDeque; use std::fs; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex as StdMutex}; use std::time::{Duration, Instant}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tokio::sync::oneshot; use tokio::time::interval; #[cfg(any(test, target_os = "windows"))] @@ -42,7 +45,7 @@ use std::ffi::OsStr; #[cfg(any(test, target_os = "windows"))] use std::path::Path; #[cfg(any(test, not(target_os = "linux")))] -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::AtomicU64; use crate::history::{ HistoryEntry, PERSISTED_HISTORY_LIMIT, SESSION_HISTORY_LIMIT, ShellHistoryFormat, @@ -63,14 +66,15 @@ use crate::{ LocalConfig, SerialBackend, SshBackend, TerminalBackend, TerminalCloseMode, TerminalEvent, TerminalSize, }; -use ssh::{ChannelEvent, RusshClient, SshChannel, SshClient}; +use ssh::{ + ChannelEvent, KeyboardInteractiveRequest, KeyboardInteractiveResponder, + KeyboardInteractiveTarget, RusshClient, SshChannel, SshClient +}; pub use ssh::{ JumpServerConnectConfig, ProxyConnectConfig, ProxyType, PtyConfig, SshAuth, SshConnectConfig, SshConnectionStage, SshSessionManager, }; -const DEFAULT_COLS: usize = 80; -const DEFAULT_ROWS: usize = 24; pub const DEFAULT_RECOVERY_SCROLLBACK_LINES: usize = 2000; pub const MAX_RECOVERY_SCROLLBACK_LINES: usize = 5000; const HISTORY_RESTORED_BANNER: &str = @@ -330,8 +334,204 @@ pub enum TerminalConnectionKind { pub struct SshTerminalConfig { pub ssh_config: SshConnectConfig, pub pty_config: PtyConfig, + /// 关闭 shell integration 注入:走裸 request_shell,失去 OSC 集成。 + pub disable_shell_integration: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TerminalMfaPrompt { + pub prompt: String, + pub echo: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TerminalMfaRequest { + pub name: String, + pub instructions: String, + pub prompts: Vec, +} + +#[derive(Clone, Default)] +pub struct TerminalMfaResponder { + state: Arc>, + event_tx: Option>, + jump_password: Option, + target_password: Option, +} + +#[derive(Default)] +struct TerminalMfaState { + pending: Option, +} + +struct TerminalMfaPending { + request: TerminalMfaRequest, + response_tx: Option>>, +} + +impl TerminalMfaResponder { + pub fn new( + event_tx: UnboundedSender, + jump_password: Option, + target_password: Option, + ) -> Self { + Self { + state: Arc::new(StdMutex::new(TerminalMfaState::default())), + event_tx: Some(event_tx), + jump_password, + target_password, + } + } + + pub fn pending_request(&self) -> Option { + self.state + .lock() + .ok()? + .pending + .as_ref() + .map(|pending| pending.request.clone()) + } + + pub fn submit(&self, responses: Vec) -> bool { + let Some(mut pending) = self + .state + .lock() + .ok() + .and_then(|mut state| state.pending.take()) + else { + return false; + }; + + let sent = pending + .response_tx + .take() + .is_some_and(|tx| tx.send(responses).is_ok()); + self.notify_changed(); + sent + } + + pub fn cancel(&self) -> bool { + let cleared = self + .state + .lock() + .ok() + .and_then(|mut state| state.pending.take()) + .is_some(); + if cleared { + self.notify_changed(); + } + cleared + } + + fn notify_changed(&self) { + if let Some(event_tx) = &self.event_tx { + let _ = event_tx.send(TerminalEvent::SshMfaChanged); + } + } +} + +#[async_trait] +impl KeyboardInteractiveResponder for TerminalMfaResponder { + async fn respond(&self, request: KeyboardInteractiveRequest) -> Result> { + let terminal_prompts = request + .prompts + .iter() + .filter(|prompt| !is_ssh_password_prompt(&prompt.prompt)) + .map(|prompt| TerminalMfaPrompt { + prompt: prompt.prompt.clone(), + echo: prompt.echo, + }) + .collect::>(); + + if terminal_prompts.is_empty() { + return keyboard_interactive_answers_for_terminal( + &request, + &[], + self.jump_password.as_deref(), + self.target_password.as_deref(), + ); + } + + let (response_tx, response_rx) = oneshot::channel(); + let terminal_request = TerminalMfaRequest { + name: request.name.clone(), + instructions: request.instructions.clone(), + prompts: terminal_prompts, + }; + + if let Ok(mut state) = self.state.lock() { + state.pending = Some(TerminalMfaPending { + request: terminal_request, + response_tx: Some(response_tx), + }); + } else { + return Err(anyhow!("failed to store SSH MFA request")); + } + self.notify_changed(); + + let responses = response_rx + .await + .map_err(|_| anyhow!("SSH MFA response was cancelled"))?; + + keyboard_interactive_answers_for_terminal( + &request, + &responses, + self.jump_password.as_deref(), + self.target_password.as_deref(), + ) + } +} + +fn keyboard_interactive_answers_for_terminal( + request: &KeyboardInteractiveRequest, + responses: &[String], + jump_password: Option<&str>, + target_password: Option<&str>, +) -> Result> { + let mut response_index = 0; + let mut answers = Vec::with_capacity(request.prompts.len()); + + for prompt in &request.prompts { + if is_ssh_password_prompt(&prompt.prompt) { + let password = match request.target { + KeyboardInteractiveTarget::JumpServer => jump_password, + KeyboardInteractiveTarget::TargetServer => target_password, + }; + answers.push( + password + .ok_or_else(|| anyhow!("SSH password prompt has no configured password"))? + .to_string(), + ); + } else { + let response = responses + .get(response_index) + .ok_or_else(|| anyhow!("SSH MFA response is missing"))?; + if response.trim().is_empty() { + return Err(anyhow!("SSH MFA response is empty")); + } + answers.push(response.clone()); + response_index += 1; + } + } + + if response_index == responses.len() { + Ok(answers) + } else { + Err(anyhow!("SSH MFA response count does not match prompts")) + } } +fn is_ssh_password_prompt(prompt: &str) -> bool { + prompt + .trim() + .trim_end_matches(':') + .to_ascii_lowercase() + .ends_with("password") +} + +const DEFAULT_COLS: usize = 80; +const DEFAULT_ROWS: usize = 24; + #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum SshProcessState { Unknown, @@ -339,6 +539,10 @@ enum SshProcessState { Busy, } +/// SSH 进程检测超时时间(秒) +/// 如果连接建立后超过此时间仍未检测到任何提示符事件,认为 shell integration 不可用 +const SSH_DETECTION_TIMEOUT_SECS: u64 = 30; + /// 将路径安全地转为 POSIX shell 单参数,避免命令注入。 pub(crate) fn shell_escape_arg(arg: &str) -> String { if arg.is_empty() { @@ -599,7 +803,26 @@ fn should_report_ssh_running_processes( ssh_prompt_detected: bool, interactive_mode_active: bool, command_submitted_without_prompt_sync: bool, + connection_established_at: Option, + ssh_detection_disabled: bool, ) -> bool { + // 如果检测机制已被禁用,不报告进程 + if ssh_detection_disabled { + return false; + } + + // 检查是否超时:连接建立后超过 30 秒仍未检测到提示符事件 + if let Some(established_at) = connection_established_at { + if !ssh_prompt_detected && established_at.elapsed().as_secs() > SSH_DETECTION_TIMEOUT_SECS { + tracing::warn!( + target: "terminal.ssh", + "SSH shell integration 未检测到(超时 {} 秒),禁用进程检查以避免误报", + SSH_DETECTION_TIMEOUT_SECS + ); + return false; + } + } + is_connected && ((ssh_prompt_detected && ssh_process_state == SshProcessState::Busy) || (interactive_mode_active && command_submitted_without_prompt_sync)) @@ -1029,6 +1252,9 @@ pub struct Terminal { /// 终端尺寸 cols: usize, rows: usize, + /// 最近一次同步给 PTY 的像素尺寸,用于 nudge_resize 重发 SIGWINCH + pixel_width: u16, + pixel_height: u16, /// SSH 配置(用于重连) ssh_config: Option, @@ -1039,9 +1265,13 @@ pub struct Terminal { /// 是否已从远端收到过 OSC 133;A/B prompt 事件。 /// 用于防御 shell integration 不工作时的永久 Busy 误报。 ssh_prompt_detected: bool, - /// 用户已提交命令,但会话尚未反馈“回到 prompt”。 + /// 用户已提交命令,但会话尚未反馈”回到 prompt”。 /// 用于补偿不支持 shell integration 的 SSH,会更保守地拦截关闭。 ssh_command_submitted_without_prompt_sync: Cell, + /// SSH 连接建立的时间,用于超时检测 + ssh_connection_established_at: Option, + /// SSH 进程检测是否已禁用(当检测到 shell integration 不可用时) + ssh_detection_disabled: Cell, /// 串口参数(用于重连) serial_params: Option, /// 事件发送器(用于 SSH 重连) @@ -1142,10 +1372,10 @@ impl TerminalScrollProxy { impl Terminal { fn new_local_disconnected(error: String, cx: &mut Context) -> Self { let (event_tx, event_rx) = unbounded_channel::(); - let (term, _event_proxy, _colors) = + let (term, event_proxy, _colors) = Self::create_term(DEFAULT_COLS, DEFAULT_ROWS, event_tx.clone()); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); Self { term, @@ -1162,11 +1392,15 @@ impl Terminal { connection_wait_started_at: None, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: None, event_tx: Some(event_tx), event_proxy: None, @@ -1255,10 +1489,10 @@ impl Terminal { #[cfg(target_os = "windows")] escape_args: true, }; - let local_backend = LocalPtyBackend::new(term.clone(), event_proxy, pty_options)?; + let local_backend = LocalPtyBackend::new(term.clone(), event_proxy.clone(), pty_options)?; let local_shell_pid = local_backend.child_pid(); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); #[cfg(target_os = "macos")] Self::spawn_local_process_tree_settler(cx); Self::spawn_local_history_loader(history_shell.as_deref(), cx); @@ -1278,11 +1512,15 @@ impl Terminal { connection_wait_started_at: None, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: None, event_tx: Some(event_tx), event_proxy: None, @@ -1357,7 +1595,7 @@ impl Terminal { let local_backend = LocalPtyClientBackend::new(request_tx, session_id.clone(), child_pid); let local_shell_pid = local_backend.child_pid(); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); #[cfg(target_os = "macos")] Self::spawn_local_process_tree_settler(cx); Self::spawn_local_history_loader(history_shell.as_deref(), cx); @@ -1377,11 +1615,15 @@ impl Terminal { connection_wait_started_at: None, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: None, event_tx: Some(event_tx), event_proxy: Some(event_proxy), @@ -1450,7 +1692,7 @@ impl Terminal { let local_shell_pid = local_backend.child_pid(); let local_cwd_file = init_local_cwd_file(config.cwd_file.clone()); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); #[cfg(target_os = "macos")] Self::spawn_local_process_tree_settler(cx); Self::spawn_local_history_loader(history_shell.as_deref(), cx); @@ -1470,11 +1712,15 @@ impl Terminal { connection_wait_started_at: None, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: None, event_tx: Some(event_tx), event_proxy: Some(event_proxy), @@ -1519,6 +1765,19 @@ impl Terminal { .to_ssh_params() .expect("StoredConnection should contain valid SSH params"); + let target_password = match &ssh_params.auth_method { + SshAuthMethod::Password { password } => Some(password.clone()), + _ => None, + }; + let jump_password = + ssh_params + .jump_server + .as_ref() + .and_then(|jump| match &jump.auth_method { + SshAuthMethod::Password { password } => Some(password.clone()), + _ => None, + }); + let auth = match ssh_params.auth_method.clone() { SshAuthMethod::Password { password } => SshAuth::Password(password), SshAuthMethod::PrivateKey { @@ -1533,6 +1792,7 @@ impl Terminal { SshAuthMethod::AutoPublicKey => SshAuth::AutoPublicKey, }; + // 构建初始化命令 let init_commands = build_ssh_init_commands( working_dir, ssh_params.default_directory.as_deref(), @@ -1540,7 +1800,7 @@ impl Terminal { sync_path_with_terminal, ); - let ssh_config = SshConnectConfig { + let mut ssh_config = SshConnectConfig { host: ssh_params.host, port: ssh_params.port, username: ssh_params.username, @@ -1588,15 +1848,20 @@ impl Terminal { }; let pty_config = PtyConfig::default(); + let (event_tx, event_rx) = unbounded_channel::(); + let ssh_mfa_responder = + TerminalMfaResponder::new(event_tx.clone(), jump_password, target_password); + ssh_config.keyboard_interactive_responder = Some(Arc::new(ssh_mfa_responder.clone())); let config = SshTerminalConfig { ssh_config, pty_config, + disable_shell_integration: ssh_params.disable_shell_integration.unwrap_or(false), }; + let ssh_session_manager = Arc::new(SshSessionManager::new(config.ssh_config.clone())); let cols = config.pty_config.width as usize; let rows = config.pty_config.height as usize; - let (event_tx, event_rx) = unbounded_channel::(); let (term, event_proxy, _colors) = Self::create_term(cols, rows, event_tx.clone()); let initial_working_dir = working_dir.map(str::to_string); @@ -1604,12 +1869,11 @@ impl Terminal { replay_term_output(&term, content.as_bytes(), None); replay_term_output(&term, HISTORY_RESTORED_BANNER.as_bytes(), None); } - let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::<()>(); + let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::(); let connection_generation = 1; - let ssh_session_manager = Arc::new(SshSessionManager::new(config.ssh_config.clone())); Self::spawn_disconnect_handler(disconnect_rx, connection_generation, cx); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); Self::spawn_ssh_connect( ssh_session_manager.clone(), config.clone(), @@ -1642,11 +1906,15 @@ impl Terminal { connection_wait_started_at: Some(Instant::now()), cols, rows, + pixel_width: 0, + pixel_height: 0, ssh_config: Some(config.clone()), ssh_session_manager: Some(ssh_session_manager), ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: None, event_tx: Some(event_tx), event_proxy: Some(event_proxy), @@ -1668,13 +1936,13 @@ impl Terminal { .expect("StoredConnection 应包含有效的 SerialParams"); let (event_tx, event_rx) = unbounded_channel::(); - let (term, _event_proxy, _colors) = + let (term, event_proxy, _colors) = Self::create_term(DEFAULT_COLS, DEFAULT_ROWS, event_tx.clone()); - let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::<()>(); + let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::(); let connection_generation = 1; Self::spawn_disconnect_handler(disconnect_rx, connection_generation, cx); - Self::spawn_event_loop(event_rx, cx); + Self::spawn_event_loop(event_rx, event_proxy.wakeup_pending_handle(), cx); Self::spawn_serial_connect( serial_params.clone(), term.clone(), @@ -1699,11 +1967,15 @@ impl Terminal { connection_wait_started_at: None, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, ssh_process_state: Cell::new(SshProcessState::Unknown), ssh_prompt_detected: false, ssh_command_submitted_without_prompt_sync: Cell::new(false), + ssh_connection_established_at: None, + ssh_detection_disabled: Cell::new(false), serial_params: Some(serial_params), event_tx: Some(event_tx), event_proxy: None, @@ -1803,7 +2075,11 @@ impl Terminal { .detach(); } - fn spawn_event_loop(mut event_rx: UnboundedReceiver, cx: &mut Context) { + fn spawn_event_loop( + mut event_rx: UnboundedReceiver, + wakeup_pending: Arc, + cx: &mut Context, + ) { let _entity = cx.entity().downgrade(); let (render_tx, mut render_rx) = futures::channel::mpsc::unbounded::(); @@ -1836,6 +2112,9 @@ impl Terminal { // 最后发送 Wakeup if pending_wakeup { pending_wakeup = false; + // 转发完毕后允许 alacritty 线程的下一次 Wakeup 重新入队, + // 避免高速输出时被 GpuiEventProxy 的去重永久吞掉 + wakeup_pending.store(false, Ordering::Release); if render_tx.unbounded_send(TerminalEvent::Wakeup).is_err() { return; } @@ -1863,25 +2142,27 @@ impl Terminal { } fn spawn_disconnect_handler( - disconnect_rx: tokio::sync::oneshot::Receiver<()>, + disconnect_rx: tokio::sync::oneshot::Receiver, generation: u64, cx: &mut Context, ) { let entity = cx.entity().downgrade(); cx.spawn(async move |_, cx| { - let _ = disconnect_rx.await; + let is_graceful = disconnect_rx.await.unwrap_or(false); let _ = entity.update(cx, |this, cx| { if !this.is_current_connection_generation(generation) { return; } + if is_graceful { + this.child_exited = Some(0); + cx.emit(TerminalModelEvent::ChildExit(0)); + } this.connection_state = ConnectionState::Disconnected { error: None }; this.connection_status_message = None; this.connection_wait_started_at = None; this.backend = None; - this.child_exited = Some(0); this.reset_ssh_process_tracking(); this.set_connection_active(false, cx); - cx.emit(TerminalModelEvent::ChildExit(0)); cx.emit(TerminalModelEvent::Wakeup); }); }) @@ -1957,17 +2238,14 @@ impl Terminal { event_proxy: GpuiEventProxy, event_tx: UnboundedSender, connection_id: Option, - on_disconnect: Option>, + on_disconnect: Option>, init_commands: Option, generation: u64, cx: &mut Context, ) { - // 创建 SSH 后端需要的通知通道 let (notify_tx, mut notify_rx) = unbounded_channel::<()>(); - let (progress_tx, mut progress_rx) = unbounded_channel::(); let task = Tokio::spawn(cx, async move { - // 转发 SSH 通知到事件通道(必须在 tokio runtime 内部) let event_tx_clone = event_tx.clone(); tokio::spawn(async move { while notify_rx.recv().await.is_some() { @@ -1976,15 +2254,15 @@ impl Terminal { }); let disconnect_tx = on_disconnect.map(|tx| { - let (sender, receiver) = tokio::sync::oneshot::channel::<()>(); + let (sender, mut receiver) = unbounded_channel::<()>(); tokio::spawn(async move { - if receiver.await.is_ok() { - let _ = tx.send(()); + if receiver.recv().await.is_some() { + let _ = tx.send(true); } }); sender }); - SshBackend::connect_with_progress( + SshBackend::connect( session_manager, config.pty_config, connection_id, @@ -1994,30 +2272,11 @@ impl Terminal { notify_tx, disconnect_tx, init_commands, - move |stage| { - let _ = progress_tx.send(stage); - }, + config.disable_shell_integration, ) .await }); - cx.spawn(async move |this: WeakEntity, cx| { - while let Some(stage) = progress_rx.recv().await { - if this - .update(cx, |this, cx| { - if matches!(this.connection_state, ConnectionState::Connecting) { - this.connection_status_message = Some(stage.description()); - cx.emit(TerminalModelEvent::Wakeup); - } - }) - .is_err() - { - break; - } - } - }) - .detach(); - cx.spawn(async move |this: WeakEntity, cx| { let result = task.await; let _ = this.update(cx, |this, cx| { @@ -2048,6 +2307,12 @@ impl Terminal { // 后端任务与这里并发执行,prompt 事件可能先于连接成功回调到达。 // 成功分支不能重置 SSH 跟踪状态,否则会把已收到的 Idle/prompt 信号抹掉, // 导致 top 等前台程序运行时无法正确拦截关闭。 + + // 记录 SSH 连接建立时间,用于超时检测 + if self.connection_kind == TerminalConnectionKind::Ssh { + self.ssh_connection_established_at = Some(Instant::now()); + } + tracing::debug!( target: "terminal.ssh", ssh_process_state = ?self.ssh_process_state.get(), @@ -2099,15 +2364,15 @@ impl Terminal { params: SerialParams, term: Arc>>, event_tx: UnboundedSender, - on_disconnect: Option>, + on_disconnect: Option>, generation: u64, cx: &mut Context, ) { let disconnect_tx = on_disconnect.map(|tx| { - let (sender, receiver) = tokio::sync::oneshot::channel::<()>(); + let (sender, receiver) = tokio::sync::oneshot::channel::(); Tokio::spawn(cx, async move { - if receiver.await.is_ok() { - let _ = tx.send(()); + if let Ok(is_graceful) = receiver.await { + let _ = tx.send(is_graceful); } }) .detach(); @@ -2368,7 +2633,11 @@ impl Terminal { } /// 是否存在会在关闭时被中断的本地子进程。 - pub fn has_running_processes(&self) -> bool { + pub fn has_running_processes(&self, check_enabled: bool) -> bool { + if !check_enabled { + return false; + } + if self.child_exited.is_some() { return false; } @@ -2401,13 +2670,32 @@ impl Terminal { let interactive_mode_active = has_ssh_interactive_program_mode(self.mode()); let command_submitted_without_prompt_sync = self.ssh_command_submitted_without_prompt_sync.get() && !self.ssh_prompt_detected; - let result = should_report_ssh_running_processes( + + // 检查是否需要禁用检测(超时) + let should_disable = should_report_ssh_running_processes( matches!(self.connection_state, ConnectionState::Connected), ssh_process_state, self.ssh_prompt_detected, interactive_mode_active, command_submitted_without_prompt_sync, + self.ssh_connection_established_at, + self.ssh_detection_disabled.get(), ); + + // 如果检测到超时,标记为已禁用 + if !self.ssh_detection_disabled.get() + && self.ssh_connection_established_at.is_some() + && !self.ssh_prompt_detected + && self.ssh_connection_established_at.unwrap().elapsed().as_secs() > SSH_DETECTION_TIMEOUT_SECS + { + self.ssh_detection_disabled.set(true); + tracing::warn!( + target: "terminal.ssh", + "SSH shell integration 检测超时,已禁用进程检查" + ); + } + + let result = should_disable; if result { tracing::debug!( target: "terminal.ssh", @@ -2526,21 +2814,33 @@ impl Terminal { /// 调整终端大小 pub fn resize(&mut self, cols: usize, rows: usize, pixel_width: u16, pixel_height: u16) { if self.cols == cols && self.rows == rows { + // 单元格行列数未变,但仍记录最新像素尺寸,供 nudge_resize 复用 + self.pixel_width = pixel_width; + self.pixel_height = pixel_height; + tracing::debug!( + target: "terminal_residue", + cols, rows, pixel_width, pixel_height, + "Terminal::resize noop (cells unchanged, pixels cached)" + ); return; } tracing::info!( - "Terminal::resize: {}x{} -> {}x{}, pixel={}x{}", + target: "terminal_residue", + "Terminal::resize: {}x{} -> {}x{}, pixel={}x{}, backend={}", self.cols, self.rows, cols, rows, pixel_width, - pixel_height + pixel_height, + self.backend.is_some() ); self.cols = cols; self.rows = rows; + self.pixel_width = pixel_width; + self.pixel_height = pixel_height; self.term.lock().resize(TermDimensions { cols, rows }); @@ -2554,6 +2854,32 @@ impl Terminal { } } + /// 重新向 PTY 后端发送当前尺寸,不修改 alacritty grid。 + /// + /// 用于在 alt screen 切换等场景下触发 SIGWINCH, + /// 让 TUI 应用(opencode/lazygit/vim 等)重新查询尺寸并刷新整屏画面, + /// 避免出现底部残留旧画面的问题。 + pub fn nudge_resize(&self) { + let Some(ref backend) = self.backend else { + tracing::warn!(target: "terminal_residue", "nudge_resize skipped: no backend"); + return; + }; + tracing::info!( + target: "terminal_residue", + cols = self.cols, + rows = self.rows, + pixel_width = self.pixel_width, + pixel_height = self.pixel_height, + "Terminal::nudge_resize -> backend.resize" + ); + backend.resize(TerminalSize { + rows: self.rows as u16, + cols: self.cols as u16, + pixel_width: self.pixel_width, + pixel_height: self.pixel_height, + }); + } + /// 重新连接 SSH 或串口 pub fn reconnect(&mut self, cx: &mut Context) { self.reconnect_internal(false, cx); @@ -2610,7 +2936,7 @@ impl Terminal { return; } - let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::<()>(); + let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::(); Self::spawn_disconnect_handler(disconnect_rx, generation, cx); Self::spawn_ssh_connect( session_manager.clone(), @@ -2643,7 +2969,7 @@ impl Terminal { self.reset_terminal_surface(); let generation = self.next_connection_generation(); - let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::<()>(); + let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::(); Self::spawn_disconnect_handler(disconnect_rx, generation, cx); Self::spawn_serial_connect( params, @@ -3101,6 +3427,8 @@ mod tests { connection_state: ConnectionState::Connected, cols: 80, rows: 24, + pixel_width: 0, + pixel_height: 0, ssh_config: None, ssh_session_manager: None, serial_params: None, diff --git a/crates/terminal_view/locales/terminal_view.yml b/crates/terminal_view/locales/terminal_view.yml index 0c4e227f4a..c2519da0bd 100644 --- a/crates/terminal_view/locales/terminal_view.yml +++ b/crates/terminal_view/locales/terminal_view.yml @@ -155,10 +155,6 @@ SSH: en: Auto Public Key zh-CN: 自动公钥认证 zh-HK: 自動公鑰認證 - auto_publickey_hint: - en: Try SSH Agent first, then default private keys under ~/.ssh. - zh-CN: 将优先尝试 SSH Agent,若不可用则尝试 ~/.ssh 下的默认私钥。 - zh-HK: 將優先嘗試 SSH Agent,若不可用則嘗試 ~/.ssh 下的預設私鑰。 key_path: en: Key Path zh-CN: 密钥路径 @@ -183,6 +179,26 @@ SSH: en: Authentication Method zh-CN: 认证方式 zh-HK: 認證方式 + auto_publickey_hint: + en: Try SSH Agent first, then default private keys under ~/.ssh. + zh-CN: 将优先尝试 SSH Agent,若不可用则尝试 ~/.ssh 下的默认私钥。 + zh-HK: 將優先嘗試 SSH Agent,若不可用則嘗試 ~/.ssh 下的預設私鑰。 + test_required_before_save: + en: Please complete a successful connection test before saving + zh-CN: 请先完成一次成功的连接测试再保存 + zh-HK: 請先完成一次成功的連線測試再保存 + retest_after_change: + en: Connection parameters changed, please test again before saving + zh-CN: 连接参数已变更,请重新测试后再保存 + zh-HK: 連線參數已變更,請重新測試後再保存 + save_failed: + en: "Failed to save SSH connection: %{error}" + zh-CN: 保存 SSH 连接失败:%{error} + zh-HK: 保存 SSH 連線失敗:%{error} + save_while_testing: + en: Please wait for the connection test to finish before saving + zh-CN: 请等待连接测试完成后再保存 + zh-HK: 請等待連線測試完成後再保存 workspace: en: Workspace zh-CN: 工作区 @@ -199,22 +215,6 @@ SSH: en: Connection test successful zh-CN: 连接测试成功 zh-HK: 連線測試成功 - save_failed: - en: "Failed to save SSH connection: %{error}" - zh-CN: 保存 SSH 连接失败:%{error} - zh-HK: 保存 SSH 連線失敗:%{error} - save_while_testing: - en: Please wait for the connection test to finish before saving - zh-CN: 请等待连接测试完成后再保存 - zh-HK: 請等待連線測試完成後再保存 - test_required_before_save: - en: Please complete a successful connection test before saving - zh-CN: 请先完成一次成功的连接测试再保存 - zh-HK: 請先完成一次成功的連線測試再保存 - retest_after_change: - en: Connection parameters changed, please test again before saving - zh-CN: 连接参数已变更,请重新测试后再保存 - zh-HK: 連線參數已變更,請重新測試後再保存 # 标签页 tab_basic: en: Basic @@ -360,6 +360,14 @@ SSH: en: Leave empty to use server default directory zh-CN: 留空则使用服务器默认目录 zh-HK: 留空則使用伺服器預設目錄 + disable_shell_integration: + en: Disable Shell Integration + zh-CN: 禁用 Shell 集成 + zh-HK: 禁用 Shell 集成 + disable_shell_integration_desc: + en: Run native login shell without OSC injection (no prompt hook, command recording, or vim mouse) + zh-CN: 走裸 login shell,不注入 OSC(失去命令记录 / prompt hook / vim 鼠标) + zh-HK: 走裸 login shell,不注入 OSC(失去命令記錄 / prompt hook / vim 鼠標) # 其他设置 remark: en: Remark @@ -1083,10 +1091,6 @@ Terminal: # SSH 会话状态 SshSession: - reconnect: - en: Reconnect - zh-CN: 重新连接 - zh-HK: 重新連接 connecting: en: Connecting... zh-CN: 连接中... @@ -1103,6 +1107,10 @@ SshSession: en: The SSH session has been disconnected. zh-CN: SSH 会话已断开连接。 zh-HK: SSH 工作階段已斷開連接。 + reconnect: + en: Reconnect + zh-CN: 重新连接 + zh-HK: 重新連接 session_ended: en: Session ended. zh-CN: 会话已结束。 diff --git a/crates/terminal_view/src/highlight_presets.rs b/crates/terminal_view/src/highlight_presets.rs index f72bdbfb71..5b3c334cf6 100644 --- a/crates/terminal_view/src/highlight_presets.rs +++ b/crates/terminal_view/src/highlight_presets.rs @@ -183,7 +183,7 @@ pub fn builtin_highlight_presets() -> Vec { preset_rule( "time_and_numbers", "clock", - r"\b:?\d{2}:\d{2}(:\d{2})?\b", + r"\b:?\d{1,2}:\d{1,2}([:|\.]\d+)?\b", Some("#22d3ee"), None, 48, @@ -192,7 +192,7 @@ pub fn builtin_highlight_presets() -> Vec { preset_rule( "time_and_numbers", "quantity", - r"\b\d+(?:\.\d+)?(?:ms|s|m|h|K|KB|MB|GB|TB|%)\b", + r"\b\d+(?:\.\d+)?(?:ms|s|m|h|B|K|KB|M|MB|G|GB|T|TB|%)\b", Some("#f59e0b"), None, 36, @@ -280,14 +280,14 @@ mod tests { assert!( preset - .rules - .iter() + .rules + .iter() .any(|rule| rule.id == "preset:ip_addresses:ipv4") ); assert!( preset - .rules - .iter() + .rules + .iter() .any(|rule| rule.id == "preset:ip_addresses:ipv6") ); } diff --git a/crates/terminal_view/src/keys.rs b/crates/terminal_view/src/keys.rs index 3453478b88..fe39cd9bbf 100644 --- a/crates/terminal_view/src/keys.rs +++ b/crates/terminal_view/src/keys.rs @@ -331,4 +331,160 @@ mod tests { "\x1ba" ); } + + #[test] + fn enter_emits_carriage_return() { + let enter = Keystroke::parse("enter").unwrap(); + assert_eq!( + to_esc_str(&enter, &TermMode::NONE, false).unwrap().as_ref(), + "\x0d" + ); + } + + #[test] + fn backspace_emits_del_by_default() { + let bs = Keystroke::parse("backspace").unwrap(); + assert_eq!( + to_esc_str(&bs, &TermMode::NONE, false).unwrap().as_ref(), + "\x7f" + ); + } + + #[test] + fn ctrl_backspace_emits_bs() { + let bs = Keystroke::parse("ctrl-backspace").unwrap(); + assert_eq!( + to_esc_str(&bs, &TermMode::NONE, false).unwrap().as_ref(), + "\x08" + ); + } + + #[test] + fn shift_tab_emits_csi_z() { + let shift_tab = Keystroke::parse("shift-tab").unwrap(); + assert_eq!( + to_esc_str(&shift_tab, &TermMode::NONE, false) + .unwrap() + .as_ref(), + "\x1b[Z" + ); + } + + #[test] + fn home_app_cursor_mode_emits_ss3() { + let home = Keystroke::parse("home").unwrap(); + assert_eq!( + to_esc_str(&home, &TermMode::NONE, false).unwrap().as_ref(), + "\x1b[H" + ); + assert_eq!( + to_esc_str(&home, &TermMode::APP_CURSOR, false) + .unwrap() + .as_ref(), + "\x1bOH" + ); + } + + #[test] + fn page_up_down_emit_csi_tilde() { + let pageup = Keystroke::parse("pageup").unwrap(); + let pagedown = Keystroke::parse("pagedown").unwrap(); + assert_eq!( + to_esc_str(&pageup, &TermMode::NONE, false) + .unwrap() + .as_ref(), + "\x1b[5~" + ); + assert_eq!( + to_esc_str(&pagedown, &TermMode::NONE, false) + .unwrap() + .as_ref(), + "\x1b[6~" + ); + } + + #[test] + fn insert_delete_emit_csi_tilde() { + let ins = Keystroke::parse("insert").unwrap(); + let del = Keystroke::parse("delete").unwrap(); + assert_eq!( + to_esc_str(&ins, &TermMode::NONE, false).unwrap().as_ref(), + "\x1b[2~" + ); + assert_eq!( + to_esc_str(&del, &TermMode::NONE, false).unwrap().as_ref(), + "\x1b[3~" + ); + } + + #[test] + fn ctrl_letter_covers_full_alphabet() { + // Ctrl-A => 0x01, Ctrl-Z => 0x1a + for (key, expected) in [("ctrl-a", 0x01u8), ("ctrl-m", 0x0d), ("ctrl-z", 0x1a)] { + let ks = Keystroke::parse(key).unwrap(); + let seq = to_esc_str(&ks, &TermMode::NONE, false).unwrap(); + assert_eq!(seq.as_ref().as_bytes(), &[expected], "{key}"); + } + } + + #[test] + fn ctrl_bracket_and_underscore_emit_c0() { + assert_eq!( + to_esc_str(&Keystroke::parse("ctrl-[").unwrap(), &TermMode::NONE, false) + .unwrap() + .as_ref() + .as_bytes(), + b"\x1b" + ); + assert_eq!( + to_esc_str(&Keystroke::parse("ctrl-_").unwrap(), &TermMode::NONE, false) + .unwrap() + .as_ref() + .as_bytes(), + b"\x1f" + ); + } + + #[test] + fn ctrl_space_emits_nul() { + let ks = Keystroke::parse("ctrl-space").unwrap(); + assert_eq!( + to_esc_str(&ks, &TermMode::NONE, false) + .unwrap() + .as_ref() + .as_bytes(), + b"\x00" + ); + } + + #[test] + fn shift_arrow_in_alt_screen_remains_none_in_normal_mode() { + // 锁定当前行为:normal screen 下 shift-arrow 不发送修饰序列 + let shift_up = Keystroke::parse("shift-up").unwrap(); + assert_eq!(to_esc_str(&shift_up, &TermMode::NONE, false), None); + } + + #[test] + fn ctrl_arrow_emits_csi_with_modifier_param_5() { + // xterm modifier param: ctrl=4 => +1 = 5 + let ctrl_right = Keystroke::parse("ctrl-right").unwrap(); + assert_eq!( + to_esc_str(&ctrl_right, &TermMode::NONE, false) + .unwrap() + .as_ref(), + "\x1b[1;5C" + ); + } + + #[test] + fn alt_arrow_emits_csi_with_modifier_param_3() { + // alt=2 => +1 = 3 + let alt_left = Keystroke::parse("alt-left").unwrap(); + assert_eq!( + to_esc_str(&alt_left, &TermMode::NONE, false) + .unwrap() + .as_ref(), + "\x1b[1;3D" + ); + } } diff --git a/crates/terminal_view/src/settings.rs b/crates/terminal_view/src/settings.rs index 954abcf09a..e75c6fffbc 100644 --- a/crates/terminal_view/src/settings.rs +++ b/crates/terminal_view/src/settings.rs @@ -45,6 +45,12 @@ pub struct TerminalSettings { pub builtin_highlights_initialized: bool, #[serde(default)] pub custom_highlights: Vec, + #[serde(default = "default_check_running_processes")] + pub check_running_processes_on_exit: bool, +} + +fn default_check_running_processes() -> bool { + true } impl Default for TerminalSettings { @@ -61,6 +67,7 @@ impl Default for TerminalSettings { confirm_high_risk_command: true, builtin_highlights_initialized: true, custom_highlights: builtin_highlight_rules(), + check_running_processes_on_exit: true, } } } diff --git a/crates/terminal_view/src/sidebar/file_manager_panel.rs b/crates/terminal_view/src/sidebar/file_manager_panel.rs index b6f81e33a5..30c5f2eb8a 100644 --- a/crates/terminal_view/src/sidebar/file_manager_panel.rs +++ b/crates/terminal_view/src/sidebar/file_manager_panel.rs @@ -1890,7 +1890,7 @@ impl FileManagerPanel { return; }; - open_remote_file_editor(full_path, client, cx); + open_remote_file_editor(full_path, client, window, cx); } // ── 渲染方法 ────────────────────────────────────────────── diff --git a/crates/terminal_view/src/ssh_form_window.rs b/crates/terminal_view/src/ssh_form_window.rs index d2977f218e..82191eae8b 100644 --- a/crates/terminal_view/src/ssh_form_window.rs +++ b/crates/terminal_view/src/ssh_form_window.rs @@ -171,6 +171,9 @@ pub struct SshFormWindow { // 云同步开关 sync_enabled: bool, + // 关闭 shell integration 注入(走裸 request_shell,失去 OSC 集成) + disable_shell_integration: bool, + is_testing: bool, test_status_message: Option, test_started_at: Option, @@ -347,6 +350,7 @@ impl SshFormWindow { let mut proxy_type = ProxyTypeSelection::default(); let mut enable_legacy_kex = false; let mut sync_enabled = true; // 默认启用云同步 + let mut disable_shell_integration = false; let mut editing_credential_ref: Option = None; if let Some(ref conn) = config.editing_connection { @@ -407,6 +411,7 @@ impl SshFormWindow { if let Some(ref script) = params.init_script { init_script_input.update(cx, |s, cx| s.set_value(script, window, cx)); } + disable_shell_integration = params.disable_shell_integration.unwrap_or(false); if let Some(ref dir) = params.sftp_local_directory { sftp_local_directory_input.update(cx, |s, cx| s.set_value(dir, window, cx)); } @@ -500,6 +505,7 @@ impl SshFormWindow { pending_key_content, last_tested_signature: None, sync_enabled, + disable_shell_integration, is_testing: false, test_status_message: None, test_started_at: None, @@ -839,6 +845,11 @@ impl SshFormWindow { init_script, sftp_local_directory, sftp_remote_directory, + disable_shell_integration: if self.disable_shell_integration { + Some(true) + } else { + None + }, jump_server, proxy, }) @@ -1298,7 +1309,7 @@ impl SshFormWindow { } /// 渲染初始化标签页 - fn render_init_tab(&self) -> impl IntoElement { + fn render_init_tab(&self, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child(self.render_form_row( @@ -1317,6 +1328,28 @@ impl SshFormWindow { &t!("SSH.sftp_remote_directory"), self.styled_input(Input::new(&self.sftp_remote_directory_input)), )) + .child( + self.render_form_row( + &t!("SSH.disable_shell_integration"), + h_flex() + .gap_2() + .child( + Checkbox::new("disable-shell-integration") + .checked(self.disable_shell_integration) + .on_click(cx.listener(|this, _, _, cx| { + this.disable_shell_integration = + !this.disable_shell_integration; + cx.notify(); + })), + ) + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(t!("SSH.disable_shell_integration_desc").to_string()), + ), + ), + ) } /// 渲染跳板机标签页 @@ -1631,7 +1664,7 @@ impl Render for SshFormWindow { .overflow_y_scroll() .child(match active_tab { 0 => self.render_basic_tab(cx).into_any_element(), - 1 => self.render_init_tab().into_any_element(), + 1 => self.render_init_tab(cx).into_any_element(), 2 => self.render_jump_server_tab(cx).into_any_element(), 3 => self.render_proxy_tab(cx).into_any_element(), 4 => self.render_advanced_tab(cx).into_any_element(), @@ -1722,6 +1755,7 @@ mod tests { enable_legacy_kex: false, default_directory: Some("/tmp".to_string()), init_script: Some("pwd".to_string()), + disable_shell_integration: None, sftp_local_directory: None, sftp_remote_directory: None, jump_server: None, diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 341bc715d6..022b22f079 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -12,7 +12,7 @@ use alacritty_terminal::grid::Dimensions; use alacritty_terminal::selection::SelectionRange; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Colors; -use alacritty_terminal::term::{RenderableContent, Term, TermDamage}; +use alacritty_terminal::term::{RenderableContent, Term, TermDamage, TermMode}; use alacritty_terminal::vte::ansi::{Color, CursorShape, NamedColor, Rgb}; use gpui::*; use std::collections::HashMap; @@ -111,6 +111,79 @@ fn is_decorative_character(ch: char) -> bool { ) } +/// 为 Unicode 块字符(U+2580..U+259F)生成几何矩形序列。 +/// +/// 返回的 rect 坐标以 cell 自身宽高的 [0, 1] 归一化系数表示, +/// 调用方在 paint 阶段乘以 cell_width / cell_height 得到像素矩形。 +/// +/// 几何绘制避免依赖字体字形,可解决字体回退时块状字符出现接缝、 +/// 抗锯齿不一致或 line-height gap 导致的视觉断层问题。 +fn block_element_geometry(c: char) -> Option> { + fn rect(x: f32, y: f32, w: f32, h: f32) -> BlockRect { + BlockRect { x, y, w, h } + } + fn lower(fraction: f32) -> Vec { + vec![rect(0.0, 1.0 - fraction, 1.0, fraction)] + } + fn left(fraction: f32) -> Vec { + vec![rect(0.0, 0.0, fraction, 1.0)] + } + const QUAD_UPPER_LEFT: u8 = 1 << 0; + const QUAD_UPPER_RIGHT: u8 = 1 << 1; + const QUAD_LOWER_LEFT: u8 = 1 << 2; + const QUAD_LOWER_RIGHT: u8 = 1 << 3; + fn quadrants(mask: u8) -> Vec { + let mut out = Vec::with_capacity(4); + if mask & QUAD_UPPER_LEFT != 0 { + out.push(rect(0.0, 0.0, 0.5, 0.5)); + } + if mask & QUAD_UPPER_RIGHT != 0 { + out.push(rect(0.5, 0.0, 0.5, 0.5)); + } + if mask & QUAD_LOWER_LEFT != 0 { + out.push(rect(0.0, 0.5, 0.5, 0.5)); + } + if mask & QUAD_LOWER_RIGHT != 0 { + out.push(rect(0.5, 0.5, 0.5, 0.5)); + } + out + } + + Some(match c { + '\u{2580}' => vec![rect(0.0, 0.0, 1.0, 0.5)], // ▀ upper half + '\u{2581}' => lower(1.0 / 8.0), // ▁ + '\u{2582}' => lower(2.0 / 8.0), // ▂ + '\u{2583}' => lower(3.0 / 8.0), // ▃ + '\u{2584}' => lower(4.0 / 8.0), // ▄ + '\u{2585}' => lower(5.0 / 8.0), // ▅ + '\u{2586}' => lower(6.0 / 8.0), // ▆ + '\u{2587}' => lower(7.0 / 8.0), // ▇ + '\u{2588}' => vec![rect(0.0, 0.0, 1.0, 1.0)], // █ full block + '\u{2589}' => left(7.0 / 8.0), // ▉ + '\u{258A}' => left(6.0 / 8.0), // ▊ + '\u{258B}' => left(5.0 / 8.0), // ▋ + '\u{258C}' => left(4.0 / 8.0), // ▌ + '\u{258D}' => left(3.0 / 8.0), // ▍ + '\u{258E}' => left(2.0 / 8.0), // ▎ + '\u{258F}' => left(1.0 / 8.0), // ▏ + '\u{2590}' => vec![rect(0.5, 0.0, 0.5, 1.0)], // ▐ right half + // U+2591..U+2593 阴影块由文本路径处理(依赖字体本身的密度图,更自然) + '\u{2594}' => vec![rect(0.0, 0.0, 1.0, 1.0 / 8.0)], // ▔ upper one-eighth + '\u{2595}' => vec![rect(7.0 / 8.0, 0.0, 1.0 / 8.0, 1.0)], // ▕ right one-eighth + '\u{2596}' => quadrants(QUAD_LOWER_LEFT), + '\u{2597}' => quadrants(QUAD_LOWER_RIGHT), + '\u{2598}' => quadrants(QUAD_UPPER_LEFT), + '\u{2599}' => quadrants(QUAD_UPPER_LEFT | QUAD_LOWER_LEFT | QUAD_LOWER_RIGHT), + '\u{259A}' => quadrants(QUAD_UPPER_LEFT | QUAD_LOWER_RIGHT), + '\u{259B}' => quadrants(QUAD_UPPER_LEFT | QUAD_UPPER_RIGHT | QUAD_LOWER_LEFT), + '\u{259C}' => quadrants(QUAD_UPPER_LEFT | QUAD_UPPER_RIGHT | QUAD_LOWER_RIGHT), + '\u{259D}' => quadrants(QUAD_UPPER_RIGHT), + '\u{259E}' => quadrants(QUAD_UPPER_RIGHT | QUAD_LOWER_LEFT), + '\u{259F}' => quadrants(QUAD_UPPER_RIGHT | QUAD_LOWER_LEFT | QUAD_LOWER_RIGHT), + _ => return None, + }) +} + /// Manages decorations from all addons pub struct DecorationManager { // Decorations indexed by line number @@ -206,6 +279,8 @@ impl DecorationManager { pub struct CachedLine { pub background_rects: Vec<(usize, usize, Hsla)>, pub text_runs: Vec, + /// 块状字符(U+2580..U+259F)使用几何绘制,避免字体回退导致的接缝 + pub block_glyphs: Vec, } #[derive(Clone)] @@ -219,6 +294,25 @@ pub struct CachedTextRun { pub char_count: usize, } +/// 单个 cell 内的几何块字符渲染数据 +/// +/// rects 中的坐标均归一化到 cell 自身的 [0, 1] 范围, +/// paint 时再按当前 cell_width/cell_height 缩放为像素矩形。 +#[derive(Clone)] +pub struct CachedBlockGlyph { + pub column: usize, + pub color: Hsla, + pub rects: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BlockRect { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, +} + /// Terminal rendering cache maintained by TerminalView pub struct RenderCache { lines: Vec, @@ -250,6 +344,23 @@ struct CachedCursor { shape: CursorShape, } +enum DamageSnapshot { + Full, + Partial(Vec), +} + +impl DamageSnapshot { + fn from_term_damage(damage: TermDamage<'_>) -> Self { + match damage { + TermDamage::Full => Self::Full, + TermDamage::Partial(iter) => { + let lines = iter.map(|line_damage| line_damage.line).collect(); + Self::Partial(lines) + } + } + } +} + impl RenderCache { pub fn new(num_lines: usize, num_cols: usize, colors: Colors) -> Self { let default_bg = convert_color(Color::Named(NamedColor::Background), &colors); @@ -257,7 +368,8 @@ impl RenderCache { lines: vec![ CachedLine { background_rects: Vec::new(), - text_runs: Vec::new() + text_runs: Vec::new(), + block_glyphs: Vec::new(), }; num_lines ], @@ -287,9 +399,20 @@ impl RenderCache { // Handle resize if num_lines != self.num_lines || num_cols != self.num_cols { + tracing::info!( + target: "terminal_residue", + old_lines = self.num_lines, + old_cols = self.num_cols, + new_lines = num_lines, + new_cols = num_cols, + "RenderCache::resize" + ); self.resize(num_lines, num_cols); } + let damage = DamageSnapshot::from_term_damage(term.damage()); + term.reset_damage(); + // Collect decorations from all addons let display_offset = term.grid().display_offset(); self.decoration_manager @@ -309,37 +432,51 @@ impl RenderCache { // 同步主题光标颜色 self.custom_cursor = theme.cursor; - // Force full rebuild when theme colors or decorations changed - let has_decorations = !self.decoration_manager.decorations_by_line.is_empty(); - if fg_changed || bg_changed || has_decorations { - self.rebuild_all(term); - self.update_last_selection(term); - return; - } - - // Check terminal color palette changes + // 在任何 full rebuild 早返回之前同步终端调色板。 let colors = term.colors(); - if !colors_equal(&self.colors, colors) { + let colors_changed = !colors_equal(&self.colors, colors); + if colors_changed { self.colors = colors.clone(); self.default_bg = convert_color(Color::Named(NamedColor::Background), &self.colors); - self.rebuild_all(term); - self.update_last_selection(term); + } + + // 主题颜色变化或存在装饰时保守全量重建。 + let has_decorations = !self.decoration_manager.decorations_by_line.is_empty(); + if fg_changed || bg_changed || colors_changed || has_decorations { + tracing::debug!( + target: "terminal_residue", + fg_changed, + bg_changed, + colors_changed, + has_decorations, + num_lines, + "rebuild_all (forced by theme/decoration)" + ); + self.rebuild_all_and_update_state(term); return; } - // Collect dirty lines from terminal damage let mut dirty_lines: std::collections::HashSet = std::collections::HashSet::new(); - let damage = term.damage(); match damage { - TermDamage::Full => { - self.rebuild_all(term); - self.update_last_selection(term); + DamageSnapshot::Full => { + tracing::debug!( + target: "terminal_residue", + num_lines, + "rebuild_all (TermDamage::Full)" + ); + self.rebuild_all_and_update_state(term); return; } - TermDamage::Partial(iter) => { - for line_damage in iter { - dirty_lines.insert(line_damage.line); + DamageSnapshot::Partial(lines) => { + if !lines.is_empty() { + tracing::debug!( + target: "terminal_residue", + damaged = ?lines, + num_lines, + "Partial damage" + ); } + dirty_lines.extend(lines); } } @@ -389,11 +526,18 @@ impl RenderCache { CachedLine { background_rects: Vec::new(), text_runs: Vec::new(), + block_glyphs: Vec::new(), }, ); self.left_edge_fingerprint.resize(num_lines, 0); } + fn rebuild_all_and_update_state(&mut self, term: &Term) { + self.rebuild_all(term); + self.update_last_selection(term); + self.sync_left_edge_fingerprint(term, 4); + } + fn rebuild_all(&mut self, term: &Term) { let content = term.renderable_content(); let display_offset = content.display_offset; @@ -403,6 +547,7 @@ impl RenderCache { for line in &mut self.lines { line.background_rects.clear(); line.text_runs.clear(); + line.block_glyphs.clear(); } // Group cells by screen line @@ -437,6 +582,34 @@ impl RenderCache { // Update cursor from a fresh content let content = term.renderable_content(); self.update_cursor_from_content(&content); + + // 调试日志:统计 cache 重建后各行的内容分布。 + // 关注底部最后 8 行,若 TUI 仅画了上半部,底部 8 行的 text/bg 应该为空。 + let total = self.lines.len(); + let non_empty_lines = self + .lines + .iter() + .filter(|l| !l.text_runs.is_empty() || !l.background_rects.is_empty()) + .count(); + let mut tail_summary = Vec::new(); + let tail_start = total.saturating_sub(8); + for idx in tail_start..total { + let l = &self.lines[idx]; + tail_summary.push(format!( + "[{idx}] bg={} text={} chars={}", + l.background_rects.len(), + l.text_runs.len(), + l.text_runs.iter().map(|r| r.char_count).sum::(), + )); + } + tracing::debug!( + target: "terminal_residue", + total_lines = total, + non_empty_lines, + in_alt_screen = content.mode.contains(TermMode::ALT_SCREEN), + tail = tail_summary.join(" | "), + "rebuild_all done" + ); } /// Rebuild specified lines @@ -481,6 +654,7 @@ impl RenderCache { if line_idx < self.num_lines { self.lines[line_idx].background_rects.clear(); self.lines[line_idx].text_runs.clear(); + self.lines[line_idx].block_glyphs.clear(); let cells = std::mem::take(&mut line_cells[line_idx]); self.build_line_cache(line_idx, cells); } @@ -538,8 +712,39 @@ impl RenderCache { term: &Term, probe_cols: usize, ) -> Vec { + let current = self.compute_left_edge_fingerprint(term, probe_cols); + + if self.left_edge_fingerprint.len() != self.num_lines { + self.left_edge_fingerprint.resize(self.num_lines, 0); + } + + let mut changed = Vec::new(); + for (line_idx, (old, new)) in self + .left_edge_fingerprint + .iter() + .zip(current.iter()) + .enumerate() + { + if old != new { + changed.push(line_idx); + } + } + + self.left_edge_fingerprint = current; + changed + } + + fn sync_left_edge_fingerprint(&mut self, term: &Term, probe_cols: usize) { + self.left_edge_fingerprint = self.compute_left_edge_fingerprint(term, probe_cols); + } + + fn compute_left_edge_fingerprint( + &self, + term: &Term, + probe_cols: usize, + ) -> Vec { if self.num_lines == 0 || probe_cols == 0 { - return Vec::new(); + return vec![0; self.num_lines]; } let mut current = vec![0_u64; self.num_lines]; @@ -567,24 +772,7 @@ impl RenderCache { .wrapping_add(piece.wrapping_add(1469598103934665603)); } - if self.left_edge_fingerprint.len() != self.num_lines { - self.left_edge_fingerprint.resize(self.num_lines, 0); - } - - let mut changed = Vec::new(); - for (line_idx, (old, new)) in self - .left_edge_fingerprint - .iter() - .zip(current.iter()) - .enumerate() - { - if old != new { - changed.push(line_idx); - } - } - - self.left_edge_fingerprint = current; - changed + current } fn build_line_cache(&mut self, line_idx: usize, mut cells: Vec) { @@ -692,6 +880,19 @@ impl RenderCache { continue; } + // 块状字符走几何路径,避免不同字体渲染出现接缝 + if let Some(rects) = block_element_geometry(cell.c) { + if let Some(run) = text_run.take() { + line.text_runs.push(run); + } + line.block_glyphs.push(CachedBlockGlyph { + column: cell.column, + color: fg, + rects, + }); + continue; + } + let bold = cell.flags.contains(Flags::BOLD); let italic = cell.flags.contains(Flags::ITALIC); @@ -980,6 +1181,16 @@ impl Element for TerminalElementImpl { let intersection = content_mask.intersect(&terminal_bounds); if intersection.size.height <= px(0.) || intersection.size.width <= px(0.) { + tracing::debug!( + target: "terminal_residue", + lines = self.lines.len(), + num_cols = self.num_cols, + cell_w = ?tb.cell_width, + cell_h = ?tb.cell_height, + origin = ?tb.origin, + content_mask = ?content_mask, + "paint skipped (no intersection)" + ); return; // 完全不可见,跳过渲染 } @@ -997,6 +1208,26 @@ impl Element for TerminalElementImpl { .ceil() as usize; let visible_end = last_visible.min(self.lines.len()); + // 仅在统计行数 / 像素差异时记录一次,避免每帧爆量 + let cm_h: f32 = content_mask.size.height.into(); + let tb_h: f32 = terminal_height.into(); + if (cm_h - tb_h).abs() > 0.5 || self.lines.len() < visible_end { + tracing::debug!( + target: "terminal_residue", + lines = self.lines.len(), + num_cols = self.num_cols, + cell_w = ?tb.cell_width, + cell_h = ?tb.cell_height, + origin = ?tb.origin, + terminal_bounds_h = ?terminal_height, + content_mask = ?content_mask, + first_visible, + visible_end, + bg_alpha = self.custom_background.a, + "paint metrics" + ); + } + // Paint backgrounds (only visible lines) for line_idx in first_visible..visible_end { let line = &self.lines[line_idx]; @@ -1009,6 +1240,24 @@ impl Element for TerminalElementImpl { } } + // Paint block-element geometry(在文字之前,与背景同样的覆盖关系) + for line_idx in first_visible..visible_end { + let line = &self.lines[line_idx]; + for glyph in &line.block_glyphs { + let cell_origin = tb.cell_origin(line_idx, glyph.column); + for r in &glyph.rects { + let rect = Bounds::new( + Point::new( + cell_origin.x + tb.cell_width * r.x, + cell_origin.y + tb.cell_height * r.y, + ), + size(tb.cell_width * r.w, tb.cell_height * r.h), + ); + window.paint_quad(fill(rect, glyph.color)); + } + } + } + // Paint text (only visible lines, using cached fonts) // 使用 cell_width 确保等宽渲染,避免字符布局漂移 for line_idx in first_visible..visible_end { diff --git a/crates/terminal_view/src/view.rs b/crates/terminal_view/src/view.rs index 4939ba934f..cf732797ed 100644 --- a/crates/terminal_view/src/view.rs +++ b/crates/terminal_view/src/view.rs @@ -248,17 +248,51 @@ fn take_whole_scroll_lines(scroll_lines_accumulated: &mut f32) -> i32 { lines } -fn alt_screen_scroll_arrow(lines: i32, app_cursor: bool) -> Option<&'static str> { +fn sgr_mouse_wheel_report(lines: i32, col: usize, row: usize) -> Option { if lines == 0 { return None; } - Some(match (lines > 0, app_cursor) { - (true, true) => "\x1bOA", // Up, application mode - (true, false) => "\x1b[A", // Up, normal mode - (false, true) => "\x1bOB", // Down, application mode - (false, false) => "\x1b[B", // Down, normal mode - }) + let button = if lines > 0 { 64 } else { 65 }; + Some(format!("\x1b[<{};{};{}M", button, col + 1, row + 1)) +} + +/// 生成 SGR 鼠标按钮报告。 +/// +/// - `button`:xterm 按钮编码(0=左键、1=中键、2=右键,加上 shift/alt/ctrl/拖动等位) +/// - `pressed`:true 用 `M` 表示按下,false 用 `m` 表示释放(SGR 协议规定) +/// - `col` / `row`:0-based,输出转为 1-based +/// +/// 抽出为独立纯函数,便于单元测试和后续扩展(拖动 32 位、wheel-with-modifiers 等)。 +fn sgr_mouse_button_report(button: u8, col: usize, row: usize, pressed: bool) -> String { + let suffix = if pressed { 'M' } else { 'm' }; + format!("\x1b[<{};{};{}{}", button, col + 1, row + 1, suffix) +} + +/// 将 GPUI 鼠标按钮映射为 xterm 按钮基础编码:左=0、中=1、右=2。 +/// 其它按钮(X1/X2 等)当前未在 SGR 报告中使用,返回 None。 +fn mouse_button_code(button: MouseButton) -> Option { + match button { + MouseButton::Left => Some(0), + MouseButton::Middle => Some(1), + MouseButton::Right => Some(2), + _ => None, + } +} + +/// 将修饰键编码到 xterm 鼠标按钮的高位:shift=4、alt=8、control=16。 +fn encode_mouse_modifiers(modifiers: Modifiers) -> u8 { + let mut bits = 0u8; + if modifiers.shift { + bits |= 4; + } + if modifiers.alt { + bits |= 8; + } + if modifiers.control { + bits |= 16; + } + bits } fn should_scroll_to_bottom_on_user_input( @@ -568,6 +602,12 @@ pub struct TerminalView { cell_width: Pixels, last_size: Option<(usize, usize)>, + /// 上一帧 alacritty 是否处于 alt screen 模式。 + /// + /// 用于检测主屏与备用屏切换:进入 alt screen 时主动调用 nudge_resize + /// 重发当前尺寸给 PTY,触发 SIGWINCH,让 TUI 应用刷新整屏画面, + /// 避免出现底部残留上一次渲染内容的问题。 + last_alt_screen: bool, scroll_lines_accumulated: f32, mouse_state: MouseState, @@ -730,7 +770,10 @@ impl TerminalView { } fn has_blocking_terminal_activity(&self, cx: &App) -> bool { - self.terminal.read(cx).has_running_processes() + let settings = current_settings(cx); + self.terminal + .read(cx) + .has_running_processes(settings.check_running_processes_on_exit) } pub fn new(config: LocalConfig, window: &mut Window, cx: &mut Context) -> Self { @@ -1005,6 +1048,7 @@ impl TerminalView { // 初始化为 None,确保首次渲染时会触发 resize, // 将正确的终端尺寸发送给 PTY last_size: None, + last_alt_screen: false, scroll_lines_accumulated: 0.0, mouse_state: MouseState::default(), addon_manager: Self::create_addon_manager(), @@ -1784,10 +1828,10 @@ impl TerminalView { // 可选:播放声音或闪烁标签 } TerminalModelEvent::ChildExit(_) => { - // 仅本地终端在 shell 退出时自动关闭标签。 - // SSH / 串口连接失败或远端会话结束时需要保留标签, - // 以便用户查看错误信息或执行重连。 - if self.connection_kind(cx) == TerminalConnectionKind::Local { + // 本地终端 shell 退出,或 SSH / 串口用户主动 exit 时自动关闭标签。 + // 网络故障等异常断开保留标签,以便用户查看错误信息或执行重连。 + let should_close = self.terminal.read(cx).child_exited().is_some(); + if should_close { self.request_close_from_event(_window, cx); } cx.notify(); @@ -3014,6 +3058,16 @@ impl TerminalView { let new_size = (cols, rows); if self.last_size != Some(new_size) { + tracing::info!( + target: "terminal_residue", + old = ?self.last_size, + new = ?new_size, + bounds_w = ?bounds.size.width, + bounds_h = ?bounds.size.height, + cell_width = ?self.cell_width, + line_height = ?self.line_height, + "resize_if_needed -> Terminal::resize" + ); self.last_size = Some(new_size); self.terminal.update(cx, |terminal, _| { terminal.resize( @@ -3355,7 +3409,7 @@ impl TerminalView { this.request_close(window, cx); })), ) - .when(can_reconnect && !is_key_changed, |el| { + .when(can_reconnect && !is_key_changed && !is_user_exit, |el| { el.child( Button::new("reconnect-btn") .label(t!("SshSession.reconnect")) @@ -3407,11 +3461,14 @@ impl TerminalView { } if mode.contains(TermMode::ALT_SCREEN) { - // ALT_SCREEN(vim、less 等):累计到整行后再转为上下箭头,避免放大小幅滚轮输入 - if let Some(arrow) = alt_screen_scroll_arrow(lines, mode.contains(TermMode::APP_CURSOR)) - { - for _ in 0..lines.abs() { - self.write_to_pty(arrow.as_bytes().to_vec(), cx); + if mode.contains(TermMode::SGR_MOUSE) && mode.intersects(TermMode::MOUSE_MODE) { + let point = self.pixel_to_point(event.position, self.terminal_bounds, cx); + if let Some(report) = + sgr_mouse_wheel_report(lines, point.column.0, point.line.0 as usize) + { + for _ in 0..lines.unsigned_abs() { + self.write_to_pty(report.as_bytes().to_vec(), cx); + } } } return; @@ -3478,6 +3535,41 @@ impl TerminalView { } } + /// 当终端启用 SGR 鼠标 + 任意鼠标报告模式时,把按钮按下/释放事件以 SGR 形式 + /// 回报给 PTY。返回 true 表示已经处理,调用方应跳过 selection/dismiss/paste 等本地行为。 + /// + /// 特殊穿透:Shift+Left 永远走终端自身的文本选区,不向 TUI 转发 —— 这是 xterm/iTerm/ + /// kitty/wezterm 等的通用约定,让用户在 vim/tmux 等捕获鼠标的应用里仍能复制文本。 + /// 同理 mouse_up 时,如果当前正在终端选区(由 shift+drag 启动),也跳过 release 回报, + /// 避免在 release 阶段 shift 已松开就把 release 事件错发给 TUI、丢掉 selection 收尾。 + fn try_report_sgr_mouse_button( + &mut self, + button: MouseButton, + position: Point, + modifiers: Modifiers, + pressed: bool, + cx: &mut Context, + ) -> bool { + if button == MouseButton::Left + && (modifiers.shift || (!pressed && self.mouse_state.selecting)) + { + return false; + } + let mode = self.terminal.read(cx).mode(); + if !(mode.contains(TermMode::SGR_MOUSE) && mode.intersects(TermMode::MOUSE_MODE)) { + return false; + } + let Some(base) = mouse_button_code(button) else { + return false; + }; + let point = self.pixel_to_point(position, self.terminal_bounds, cx); + let encoded = base | encode_mouse_modifiers(modifiers); + let report = + sgr_mouse_button_report(encoded, point.column.0, point.line.0 as usize, pressed); + self.write_to_pty(report.into_bytes(), cx); + true + } + fn handle_mouse_down( &mut self, event: &MouseDownEvent, @@ -3572,10 +3664,20 @@ impl TerminalView { fn handle_middle_mouse_down( &mut self, - _event: &MouseDownEvent, + event: &MouseDownEvent, window: &mut Window, cx: &mut Context, ) { + // SGR 鼠标模式下中键按下走 TUI 报告而不是 middle-click paste + if self.try_report_sgr_mouse_button( + MouseButton::Middle, + event.position, + event.modifiers, + true, + cx, + ) { + return; + } if !self.middle_click_paste { return; } @@ -3644,6 +3746,16 @@ impl TerminalView { _window: &mut Window, cx: &mut Context, ) { + // SGR 鼠标模式下:先回报释放,然后跳过 selection 收尾 + if self.try_report_sgr_mouse_button( + event.button, + event.position, + event.modifiers, + false, + cx, + ) { + return; + } if event.button != MouseButton::Left { return; } @@ -4106,6 +4218,28 @@ impl Render for TerminalView { let view = cx.entity().clone(); let show_scrollbar = !terminal_mode.contains(TermMode::ALT_SCREEN) && history_size > 0; + // 检测主屏 ↔ alt screen 切换。 + // 进入 alt screen 时(opencode/lazygit/vim 等 TUI 启动),主动重发当前尺寸到 PTY, + // 触发 SIGWINCH 让 TUI 重新查询尺寸并刷新整屏,避免底部残留旧画面。 + // 仅在 last_size 已就绪时(说明 PTY 已收到过正确尺寸)才 nudge, + // 避免覆盖即将到来的首次 resize_if_needed。 + let alt_screen = terminal_mode.contains(TermMode::ALT_SCREEN); + if alt_screen != self.last_alt_screen { + tracing::info!( + target: "terminal_residue", + from = self.last_alt_screen, + to = alt_screen, + last_size = ?self.last_size, + "alt_screen mode transition" + ); + self.last_alt_screen = alt_screen; + if alt_screen && self.last_size.is_some() { + tracing::info!(target: "terminal_residue", "nudge_resize fired on enter alt_screen"); + self.terminal + .update(cx, |terminal, _| terminal.nudge_resize()); + } + } + div() .size_full() .flex() @@ -4527,10 +4661,80 @@ mod tests { } #[test] - fn alt_screen_scroll_arrow_maps_negative_lines_to_down() { - assert_eq!(alt_screen_scroll_arrow(-1, false), Some("\x1b[B")); - assert_eq!(alt_screen_scroll_arrow(-1, true), Some("\x1bOB")); - assert_eq!(alt_screen_scroll_arrow(0, false), None); + fn sgr_mouse_wheel_report_maps_negative_lines_to_wheel_down() { + assert_eq!( + sgr_mouse_wheel_report(-1, 4, 2).as_deref(), + Some("\x1b[<65;5;3M") + ); + assert_eq!(sgr_mouse_wheel_report(0, 4, 2), None); + } + + #[test] + fn sgr_mouse_button_report_uses_capital_m_on_press() { + // 左键按下,列 0、行 0 -> 转 1-based + let s = sgr_mouse_button_report(0, 0, 0, true); + assert_eq!(s, "\x1b[<0;1;1M"); + } + + #[test] + fn sgr_mouse_button_report_uses_lowercase_m_on_release() { + let s = sgr_mouse_button_report(2, 9, 4, false); + // 右键 (button=2) 释放在 1-based col=10 row=5 + assert_eq!(s, "\x1b[<2;10;5m"); + } + + #[test] + fn sgr_mouse_button_report_supports_modifier_encoded_buttons() { + // 左键 + shift (4) + ctrl (16) -> button=20 + let s = sgr_mouse_button_report(20, 0, 0, true); + assert_eq!(s, "\x1b[<20;1;1M"); + } + + #[test] + fn sgr_mouse_button_report_supports_drag_button_codes() { + // 拖动事件:button + 32(xterm 拖动位) + // 左键拖动 = 32 + let s = sgr_mouse_button_report(32, 7, 11, true); + assert_eq!(s, "\x1b[<32;8;12M"); + } + + #[test] + fn mouse_button_code_maps_three_main_buttons() { + assert_eq!(mouse_button_code(MouseButton::Left), Some(0)); + assert_eq!(mouse_button_code(MouseButton::Middle), Some(1)); + assert_eq!(mouse_button_code(MouseButton::Right), Some(2)); + } + + #[test] + fn encode_mouse_modifiers_packs_shift_alt_control() { + let none = Modifiers::default(); + assert_eq!(encode_mouse_modifiers(none), 0); + + let shift = Modifiers { + shift: true, + ..Default::default() + }; + assert_eq!(encode_mouse_modifiers(shift), 4); + + let alt = Modifiers { + alt: true, + ..Default::default() + }; + assert_eq!(encode_mouse_modifiers(alt), 8); + + let ctrl = Modifiers { + control: true, + ..Default::default() + }; + assert_eq!(encode_mouse_modifiers(ctrl), 16); + + let all = Modifiers { + shift: true, + alt: true, + control: true, + ..Default::default() + }; + assert_eq!(encode_mouse_modifiers(all), 28); } #[test] diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index db8d0ebd38..4f6788f97a 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -52,6 +52,7 @@ pub struct Input { selected: bool, disable_ime: bool, rounded: Option, + bare: bool, } impl Sizable for Input { @@ -93,6 +94,7 @@ impl Input { selected: false, disable_ime: false, rounded: None, + bare: false, } } @@ -168,6 +170,13 @@ impl Input { self } + /// 纯编辑器模式:去掉 Input 自带的 padding、height、items_center 等布局样式, + /// 完全由父容器控制布局。用于嵌入表格单元格等场景。 + pub fn bare(mut self) -> Self { + self.bare = true; + self + } + /// Set the tab index for the input, default is 0. pub fn tab_index(mut self, index: isize) -> Self { self.tab_index = index; @@ -400,14 +409,14 @@ impl RenderOnce for Input { .on_mouse_move(window.listener_for(&self.state, InputState::on_mouse_move)) .on_scroll_wheel(window.listener_for(&self.state, InputState::on_scroll_wheel)) .size_full() - .line_height(LINE_HEIGHT) - .input_px(self.size) - .input_py(self.size) - .input_h(self.size) + .when(!self.bare, |this| this.line_height(LINE_HEIGHT)) .input_text_size(self.size) + .when(!self.bare, |this| this.input_px(self.size)) + .when(!self.bare, |this| this.input_py(self.size)) + .when(!self.bare, |this| this.input_h(self.size)) .when(!self.disabled, |this| this.cursor_text()) - .items_center() - .when(state.mode.is_multi_line(), |this| { + .when(!self.bare, |this| this.items_center()) + .when(state.mode.is_multi_line() && !self.bare, |this| { this.h_auto() .when_some(self.height, |this, height| this.h(height)) }) @@ -425,7 +434,7 @@ impl RenderOnce for Input { this.rounded(cx.theme().radius) }) }) - .items_center() + .when(!self.bare, |this| this.items_center()) .gap(gap_x) .refine_style(&self.style) .when( diff --git a/crates/ui/src/input/lsp/completions.rs b/crates/ui/src/input/lsp/completions.rs index b594281dac..59d56a4fc6 100644 --- a/crates/ui/src/input/lsp/completions.rs +++ b/crates/ui/src/input/lsp/completions.rs @@ -30,11 +30,19 @@ fn completion_menu_action( new_offset: usize, start_offset: usize, ) -> CompletionMenuAction { + if new_offset < start_offset { + return if has_existing_menu { + CompletionMenuAction::Hide + } else { + CompletionMenuAction::Ignore + }; + } + if !has_existing_menu && !is_trigger { return CompletionMenuAction::Ignore; } - if has_existing_menu && (full_text.trim().is_empty() || new_offset < start_offset) { + if has_existing_menu && full_text.trim().is_empty() { return CompletionMenuAction::Hide; } @@ -152,7 +160,7 @@ impl InputState { // It will check if menu is open before showing the suggestion. self.schedule_inline_completion(window, cx); - let start = range.end; + let start = range.start; let new_offset = self.cursor(); let existing_menu = match self.context_menu.as_ref() { Some(ContextMenu::Completion(menu)) => Some(menu), @@ -390,6 +398,14 @@ mod tests { ); } + #[test] + fn ignores_trigger_without_existing_menu_when_cursor_is_before_trigger_start() { + assert_eq!( + completion_menu_action(false, true, "n", 1, 3), + CompletionMenuAction::Ignore + ); + } + #[test] fn refreshes_existing_menu_on_delete_when_text_still_has_context() { assert_eq!( diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 4c51bfbd04..7251014105 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -10,7 +10,7 @@ use crate::{ use gpui::{ AnyView, App, AppContext, Context, DefiniteLength, Entity, FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Pixels, Render, StyleRefinement, Styled, - WeakFocusHandle, Window, actions, div, prelude::FluentBuilder as _, + WeakFocusHandle, Window, actions, div, prelude::FluentBuilder as _, px }; use std::{any::TypeId, cell::RefCell, rc::Rc}; @@ -467,9 +467,9 @@ impl Render for Root { .relative() .size_full() .font_family(cx.theme().font_family.clone()) - .bg(cx.theme().transparent) - .rounded(cx.theme().radius_lg) - .overflow_hidden() + .bg(cx.theme().background) + .rounded(cx.theme().radius_lg - px(4.0)) + // .overflow_hidden() .text_color(cx.theme().foreground) .refine_style(&self.style) .child(self.view.clone()), diff --git a/crates/ui/src/table/delegate.rs b/crates/ui/src/table/delegate.rs index 2ea2cd5863..257a0fa611 100644 --- a/crates/ui/src/table/delegate.rs +++ b/crates/ui/src/table/delegate.rs @@ -8,7 +8,7 @@ use gpui::{ use crate::{ ActiveTheme as _, Icon, IconName, Size, h_flex, menu::PopupMenu, - table::{Column, ColumnSort, TableModel, TableState, loading::Loading}, + table::{Column, ColumnSort, TableState, loading::Loading}, }; /// A delegate trait for providing data and rendering for a table. @@ -198,56 +198,3 @@ pub trait TableDelegate: Sized + 'static { String::new() } } - -/// Blanket implementation of TableModel for all TableDelegate types. -/// This allows both TableDelegate and EditTableDelegate to be used through the TableModel trait. -impl TableModel for D { - type Column = Column; - - fn columns_count(&self) -> usize { - // Default implementation - requires App context in actual use - 0 - } - - fn rows_count(&self) -> usize { - // Default implementation - requires App context in actual use - 0 - } - - fn column(&self, _index: usize) -> Column { - // Default implementation - requires App context in actual use - Column::default() - } - - fn perform_sort(&mut self, _column: usize, _ascending: bool) { - // Full implementation requires Window and Context - } - - fn move_column(&mut self, _from: usize, _to: usize) { - // Full implementation requires Window and Context - } - - fn loading(&self) -> bool { - false - } - - fn has_more(&self) -> bool { - false - } - - fn load_more_threshold(&self) -> Option { - Some(20) - } - - fn load_more(&mut self) { - // Full implementation requires Window and Context - } - - fn visible_rows_changed(&mut self, _range: Range) { - // Full implementation requires Window and Context - } - - fn visible_columns_changed(&mut self, _range: Range) { - // Full implementation requires Window and Context - } -} diff --git a/crates/ui/src/table/mod.rs b/crates/ui/src/table/mod.rs index 0f7f1878cc..7608072bd6 100644 --- a/crates/ui/src/table/mod.rs +++ b/crates/ui/src/table/mod.rs @@ -1,5 +1,3 @@ -use std::ops::Range; - use crate::{ ActiveTheme, Sizable, Size, actions::{ @@ -22,66 +20,6 @@ pub use delegate::*; pub use state::*; const CONTEXT: &'static str = "Table"; - -/// A trait that defines the data model interface for tables. -/// -/// This trait extracts the common data-related functionality from both -/// `TableDelegate` and `EditTableDelegate`, allowing for shared behavior -/// between the basic `Table` and the editable `EditTable` components. -/// -/// The trait uses an associated type `Column` to allow each implementation -/// to use its own column type without requiring a unified Column struct. -#[allow(unused)] -pub trait TableModel: Send { - /// The column type used by this table model. - type Column; - - /// Return the number of columns in the table. - fn columns_count(&self) -> usize; - - /// Return the number of rows in the table. - fn rows_count(&self) -> usize; - - /// Returns the table column at the given index. - fn column(&self, index: usize) -> Self::Column; - - /// Perform sort on the column at the given index. - fn perform_sort(&mut self, column: usize, ascending: bool); - - /// Move the column at the given index to a new position. - fn move_column(&mut self, from: usize, to: usize); - - /// Return true if the table is currently loading data. - fn loading(&self) -> bool; - - /// Return a view to display while loading, if any. - fn render_loading(&self) -> Option { - None - } - - /// Return true if there is more data to load (for infinite scroll). - fn has_more(&self) -> bool { - false - } - - /// Returns the threshold (in rows) that triggers loading more data. - /// - /// When the visible range is within this many rows from the end, - /// `load_more` will be called. - fn load_more_threshold(&self) -> Option { - Some(20) - } - - /// Load more data when triggered by scroll position. - fn load_more(&mut self) {} - - /// Called when the visible range of rows changes. - fn visible_rows_changed(&mut self, range: Range) {} - - /// Called when the visible range of columns changes. - fn visible_columns_changed(&mut self, range: Range) {} -} - pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("escape", Cancel, Some(CONTEXT)), diff --git a/er.png b/er.png new file mode 100644 index 0000000000..5cb6c3ba06 Binary files /dev/null and b/er.png differ diff --git a/main/Cargo.toml b/main/Cargo.toml index 85442412a2..e52601a14d 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "main" -version = "0.3.3" +version = "0.4.1" publish.workspace = true edition.workspace = true @@ -19,6 +19,7 @@ semver = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" rust-i18n = { workspace = true } base64 = { workspace = true } dirs = {workspace = true} diff --git a/main/locales/main.yml b/main/locales/main.yml index f2aff6f6fb..4a9bb414fd 100644 --- a/main/locales/main.yml +++ b/main/locales/main.yml @@ -1129,6 +1129,14 @@ Settings: en: Reuse terminal session content when available; when disabled, only reopen the connection zh-CN: 可用时恢复终端中的会话内容;关闭后仅重新打开连接 zh-HK: 可用時恢復終端中的工作階段內容;關閉後僅重新開啟連線 + check_running_processes_on_exit: + en: Check running processes on exit + zh-CN: 退出时检测运行中的进程 + zh-HK: 退出時檢測運行中的進程 + check_running_processes_on_exit_desc: + en: Detect running processes when closing terminal tabs to prevent accidental data loss + zh-CN: 关闭终端标签页时检测是否有运行中的进程,防止意外丢失数据 + zh-HK: 關閉終端標籤頁時檢測是否有運行中的進程,防止意外丟失數據 # 数据库设置 Database: @@ -1201,6 +1209,34 @@ Settings: zh-CN: 根据首条消息由 AI 生成会话标题 zh-HK: 根據首條消息由 AI 生成會話標題 + Log: + group_title: + en: Log + zh-CN: 日志 + zh-HK: 日誌 + file_path: + en: Log File Path + zh-CN: 日志保存路径 + zh-HK: 日誌保存路徑 + file_path_desc: + en: Leave empty to write logs to the default config directory logs folder. Restart the app after changing this path. + zh-CN: 留空时写入默认配置目录下的 logs 文件夹,修改后重启应用生效。 + zh-HK: 留空時寫入默認配置目錄下的 logs 文件夾,修改後重啟應用生效。 + + Log: + group_title: + en: Log + zh-CN: 日志 + zh-HK: 日誌 + file_path: + en: Log File Path + zh-CN: 日志保存路径 + zh-HK: 日誌保存路徑 + file_path_desc: + en: Leave empty to write logs to the default config directory logs folder. Restart the app after changing this path. + zh-CN: 留空时写入默认配置目录下的 logs 文件夹,修改后重启应用生效。 + zh-HK: 留空時寫入默認配置目錄下的 logs 文件夾,修改後重啟應用生效。 + Update: group_title: en: Update diff --git a/main/src/home/home_connection_quick_open.rs b/main/src/home/home_connection_quick_open.rs index 04b630aaad..df07b1001c 100644 --- a/main/src/home/home_connection_quick_open.rs +++ b/main/src/home/home_connection_quick_open.rs @@ -84,7 +84,7 @@ impl ListDelegate for ConnectionQuickOpenDelegate { ListItem::new(ix) .px_3() .py_2() - .rounded(Radius::Md.px()) + .rounded(cx.theme().radius_lg) .on_click(move |_, window, cx| { parent.update(cx, |this, cx| { this.open_connection_from_quick(&connection_for_open, window, cx); diff --git a/main/src/home/home_new_connection.rs b/main/src/home/home_new_connection.rs deleted file mode 100644 index bb6095fb07..0000000000 --- a/main/src/home/home_new_connection.rs +++ /dev/null @@ -1,226 +0,0 @@ -use crate::home_tab::HomePage; -use gpui::{App, Context, Entity, ParentElement, SharedString, Styled, Task, Window, div}; -use gpui_component::{ - ActiveTheme, IndexPath, WindowExt, h_flex, - list::{ListDelegate, ListItem, ListState}, - tokens::Radius, -}; -use one_core::storage::DatabaseType; -use rust_i18n::t; - -/// 新建连接对话框中的连接类型选项 -#[derive(Clone)] -enum NewConnectionKind { - Workspace, - Ssh, - Terminal, - Redis, - MongoDB, - Serial, - Database(DatabaseType), -} - -impl NewConnectionKind { - fn label(&self) -> String { - match self { - NewConnectionKind::Workspace => t!("Workspace.label").to_string(), - NewConnectionKind::Ssh => "SSH".to_string(), - NewConnectionKind::Terminal => "Terminal".to_string(), - NewConnectionKind::Redis => "Redis".to_string(), - NewConnectionKind::MongoDB => "MongoDB".to_string(), - NewConnectionKind::Serial => t!("Serial.new").to_string(), - NewConnectionKind::Database(db_type) => db_type.as_str().to_string(), - } - } - - fn category(&self) -> String { - match self { - NewConnectionKind::Workspace => t!("NewConnection.workspace").to_string(), - NewConnectionKind::Ssh | NewConnectionKind::Terminal | NewConnectionKind::Serial => { - t!("NewConnection.terminal").to_string() - } - NewConnectionKind::Redis | NewConnectionKind::MongoDB => "NoSQL".to_string(), - NewConnectionKind::Database(_) => t!("NewConnection.database").to_string(), - } - } - - /// 在 HomePage 上执行对应的操作 - fn execute(&self, home: &mut HomePage, window: &mut Window, cx: &mut Context) { - match self { - NewConnectionKind::Workspace => { - home.show_workspace_form(None, window, cx); - } - NewConnectionKind::Ssh => { - home.editing_connection_id = None; - home.show_ssh_form(window, cx); - } - NewConnectionKind::Terminal => { - home.add_terminal_tab(window, cx); - } - NewConnectionKind::Redis => { - home.editing_connection_id = None; - home.show_redis_form(window, cx); - } - NewConnectionKind::MongoDB => { - home.editing_connection_id = None; - home.show_mongodb_form(window, cx); - } - NewConnectionKind::Serial => { - home.editing_connection_id = None; - home.show_serial_form(window, cx); - } - NewConnectionKind::Database(db_type) => { - home.editing_connection_id = None; - home.show_connection_form(*db_type, window, cx); - } - } - } -} - -pub(crate) struct NewConnectionDelegate { - parent: Entity, - items: Vec, - filtered_items: Vec, - selected_index: Option, - search_query: String, -} - -impl NewConnectionDelegate { - pub(crate) fn new(parent: Entity) -> Self { - let mut items = vec![ - NewConnectionKind::Workspace, - NewConnectionKind::Ssh, - NewConnectionKind::Terminal, - NewConnectionKind::Redis, - NewConnectionKind::MongoDB, - NewConnectionKind::Serial, - ]; - - for db_type in DatabaseType::all() { - items.push(NewConnectionKind::Database(*db_type)); - } - - let filtered_items = items.clone(); - - Self { - parent, - items, - filtered_items, - selected_index: None, - search_query: String::new(), - } - } - - fn apply_filter(&mut self) { - if self.search_query.is_empty() { - self.filtered_items = self.items.clone(); - return; - } - let query = self.search_query.to_lowercase(); - self.filtered_items = self - .items - .iter() - .filter(|kind| { - kind.label().to_lowercase().contains(&query) - || kind.category().to_lowercase().contains(&query) - }) - .cloned() - .collect(); - } -} - -impl ListDelegate for NewConnectionDelegate { - type Item = ListItem; - - fn perform_search( - &mut self, - query: &str, - _window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - self.search_query = query.to_string(); - self.apply_filter(); - cx.notify(); - Task::ready(()) - } - - fn items_count(&self, _section: usize, _cx: &App) -> usize { - self.filtered_items.len() - } - - fn render_item( - &mut self, - ix: IndexPath, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let kind = self.filtered_items.get(ix.row)?.clone(); - let parent = self.parent.clone(); - let label = kind.label(); - let category = kind.category(); - - Some( - ListItem::new(ix) - .px_3() - .py_2() - .rounded(Radius::Md.px()) - .on_click(move |_, window, cx| { - parent.update(cx, |this, cx| { - kind.execute(this, window, cx); - }); - window.close_dialog(cx); - }) - .child( - h_flex() - .w_full() - .items_center() - .gap_2() - .child( - div() - .flex_1() - .min_w_0() - .text_sm() - .text_ellipsis() - .whitespace_nowrap() - .child(SharedString::from(label)), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child(SharedString::from(category)), - ), - ), - ) - } - - fn set_selected_index( - &mut self, - ix: Option, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_index = ix; - } - - fn confirm( - &mut self, - _secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { - if let Some(ix) = self.selected_index { - if let Some(kind) = self.filtered_items.get(ix.row).cloned() { - let parent = self.parent.clone(); - parent.update(cx, |this, cx| { - kind.execute(this, window, cx); - }); - window.close_dialog(cx); - } - } - } - - fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { - window.close_dialog(cx); - } -} diff --git a/main/src/home/home_workspace_filter.rs b/main/src/home/home_workspace_filter.rs index 9e697a681e..2152c8b6b0 100644 --- a/main/src/home/home_workspace_filter.rs +++ b/main/src/home/home_workspace_filter.rs @@ -182,7 +182,7 @@ impl ListDelegate for WorkspaceFilterDelegate { ListItem::new(ix) .px_3() .py_2() - .rounded(Radius::Sm.px()) + .rounded(cx.theme().radius_lg) .on_click(move |_, _, cx| { parent.update(cx, |this, cx| { this.toggle_workspace_filter(item_id, cx); diff --git a/main/src/onetcli_app.rs b/main/src/onetcli_app.rs index f53ef2f928..99e780ef6e 100644 --- a/main/src/onetcli_app.rs +++ b/main/src/onetcli_app.rs @@ -145,12 +145,16 @@ use gpui_component::dock::{ClosePanel, ToggleZoom}; use gpui_component::{ActiveTheme, Icon, IconName, Root, Sizable, h_flex, v_flex}; use one_core::llm::manager::GlobalProviderState; use one_core::storage::ActiveConnections; +use one_core::storage::manager::get_config_dir; use one_core::tab_container::{ TabContainer, TabContainerEvent, TabContainerState, TabContentRegistry, TabItem, }; use one_core::tab_persistence::{load_tab_state, save_tab_state, schedule_save}; use one_core::utils::debouncer::Debouncer; use one_core::{PendingChangeLevel, RunningKind, RunningState}; +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; use rust_i18n::t; use terminal_view::with_recovery_snapshot_overrides; use tracing_subscriber::layer::SubscriberExt; @@ -469,6 +473,21 @@ fn request_main_window_close(window: &mut Window, cx: &mut App) -> bool { AppCloseDecision::Allow | AppCloseDecision::ForceClose => true, AppCloseDecision::Ignore => false, AppCloseDecision::Prompt => { + // 在打开对话框之前先保存状态,确保"仍然退出"时可以恢复 + tracing::info!("准备显示退出确认对话框,先保存当前标签状态"); + let state = with_recovery_snapshot_overrides( + cx, + None, + Some(WINDOW_CLOSE_TERMINAL_RECOVERY_MAX_CHARS), + |cx| tab_container.read(cx).dump(cx), + ); + if let Err(err) = save_tab_state(&state) { + tracing::error!("保存标签状态失败:{:?}", err); + } else { + tracing::info!("标签状态保存成功,共 {} 个标签", state.tabs.len()); + } + AppSettings::save_global(cx); + open_app_close_dialog(window, tab_container, running_states, guard, cx); false } @@ -632,16 +651,70 @@ fn request_app_close_without_window(cx: &mut App) -> bool { } } -pub fn init(cx: &mut App) { - // 从 RUST_LOG 环境变量读取日志级别,默认 info +fn init_tracing(settings: &AppSettings) { let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); - tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer()) - .with(env_filter) - .init(); + match configured_log_file_path(&settings.log_file_path) { + Ok(log_file_path) => match log_file_appender(&log_file_path) { + Ok(file_appender) => { + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + Box::leak(Box::new(guard)); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking)) + .with(env_filter) + .init(); + } + Err(err) => { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(env_filter) + .init(); + tracing::error!(path = %log_file_path.display(), error = %err, "日志文件初始化失败"); + } + }, + Err(err) => { + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(env_filter) + .init(); + tracing::error!(error = %err, "默认日志目录初始化失败"); + } + } +} + +fn configured_log_file_path(value: &str) -> anyhow::Result { + let trimmed = value.trim(); + if trimmed.is_empty() { + Ok(default_log_file_path()?) + } else { + Ok(PathBuf::from(trimmed)) + } +} + +fn default_log_file_path() -> anyhow::Result { + Ok(get_config_dir()?.join("logs").join("onetcli.log")) +} + +fn log_file_appender(path: &Path) -> std::io::Result { + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + std::fs::create_dir_all(parent)?; + } + + let mut options = std::fs::OpenOptions::new(); + options.create(true).append(true); + #[cfg(unix)] + options.mode(0o600); + options.open(path) +} + +pub fn init(cx: &mut App) { let settings = AppSettings::load(); + init_tracing(&settings); let http_client = build_app_http_client(&settings.global_proxy).expect("HTTP 客户端初始化失败"); cx.set_http_client(http_client); gpui_component::init(cx); @@ -985,14 +1058,21 @@ impl OnetCliApp { cx.on_app_quit({ let tab_container = tab_container.clone(); move |_, cx| { - let state = with_recovery_snapshot_overrides( - cx, - None, - Some(WINDOW_CLOSE_TERMINAL_RECOVERY_MAX_CHARS), - |cx| tab_container.read(cx).dump(cx), - ); - if let Err(err) = save_tab_state(&state) { - tracing::error!("退出时保存标签状态失败:{:?}", err); + // 检查是否有标签页,如果没有说明已经在 force_close 中保存过了 + let has_tabs = tab_container.read(cx).tabs().len() > 0; + if has_tabs { + tracing::info!("应用退出:保存标签状态"); + let state = with_recovery_snapshot_overrides( + cx, + None, + Some(WINDOW_CLOSE_TERMINAL_RECOVERY_MAX_CHARS), + |cx| tab_container.read(cx).dump(cx), + ); + if let Err(err) = save_tab_state(&state) { + tracing::error!("退出时保存标签状态失败:{:?}", err); + } + } else { + tracing::info!("应用退出:标签页已清空,跳过保存(状态已在强制退出时保存)"); } AppSettings::save_global(cx); let redis_state = cx @@ -1400,6 +1480,71 @@ impl OnetCliApp { } } +#[cfg(test)] +mod tests { + use super::{configured_log_file_path, default_log_file_path, log_file_appender}; + use std::io::Write; + + #[test] + fn configured_log_file_path_uses_default_for_empty_value() { + let default_path = default_log_file_path().expect("应返回默认日志路径"); + + assert_eq!(configured_log_file_path("").unwrap(), default_path); + assert_eq!(configured_log_file_path(" ").unwrap(), default_path); + } + + #[test] + fn configured_log_file_path_trims_value() { + let path = configured_log_file_path(" /tmp/onetcli.log ").expect("应返回日志路径"); + assert_eq!(path, std::path::PathBuf::from("/tmp/onetcli.log")); + } + + #[test] + fn log_file_appender_creates_parent_directories_and_appends() { + let path = std::env::temp_dir() + .join(format!("onetcli-log-test-{}", std::process::id())) + .join("nested") + .join("app.log"); + + { + let mut file = log_file_appender(&path).expect("应创建日志文件"); + writeln!(file, "first").expect("应写入第一行"); + } + { + let mut file = log_file_appender(&path).expect("应重新打开日志文件"); + writeln!(file, "second").expect("应追加第二行"); + } + + let content = std::fs::read_to_string(&path).expect("应读取日志文件"); + assert_eq!(content, "first\nsecond\n"); + + let _ = std::fs::remove_dir_all(path.parent().unwrap().parent().unwrap()); + } + + #[cfg(unix)] + #[test] + fn log_file_appender_creates_private_file() { + use std::os::unix::fs::PermissionsExt; + + let path = std::env::temp_dir() + .join(format!( + "onetcli-log-permission-test-{}", + std::process::id() + )) + .join("app.log"); + let _file = log_file_appender(&path).expect("应创建日志文件"); + + let mode = std::fs::metadata(&path) + .expect("应读取日志文件元数据") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); + + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } +} + impl Render for OnetCliApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let next_window_title = { diff --git a/main/src/setting_tab.rs b/main/src/setting_tab.rs index ba368d8afb..f5a21f2872 100644 --- a/main/src/setting_tab.rs +++ b/main/src/setting_tab.rs @@ -51,6 +51,7 @@ use terminal_view::{ DEFAULT_LINE_HEIGHT_SCALE, DEFAULT_RECOVERY_SCROLLBACK_LINES, MAX_LINE_HEIGHT_SCALE, MAX_RECOVERY_SCROLLBACK_LINES, MIN_LINE_HEIGHT_SCALE, TerminalSettings, TerminalTheme, set_recovery_scrollback_lines, + settings::{GlobalTerminalSettings, TerminalSettingsStore}, }; use tracing::{error, info}; @@ -496,6 +497,10 @@ pub struct AppSettings { #[serde(default = "default_true")] pub terminal_confirm_high_risk_command: bool, #[serde(default = "default_true")] + pub terminal_check_running_processes_on_exit: bool, + #[serde(default)] + pub log_file_path: String, + #[serde(default = "default_true")] pub restore_connections_on_startup: bool, #[serde(default = "default_true")] pub restore_session_content: bool, @@ -832,8 +837,10 @@ impl Default for AppSettings { terminal_recovery_scrollback_lines: default_terminal_recovery_scrollback_lines(), terminal_confirm_multiline_paste: default_true(), terminal_confirm_high_risk_command: default_true(), + terminal_check_running_processes_on_exit: default_true(), restore_connections_on_startup: default_true(), restore_session_content: default_true(), + log_file_path: String::new(), auto_update: true, sync_server_url: String::new(), sync_backend_type: default_sync_backend_type(), @@ -1245,6 +1252,16 @@ fn migrate_legacy_theme_state(settings: &mut AppSettings) { fn sync_terminal_settings_to_all(settings: AppSettings, cx: &mut App) { set_recovery_scrollback_lines(cx, settings.normalized_terminal_recovery_scrollback_lines()); + // 更新 GlobalTerminalSettings + if let Some(global) = cx.try_global::() { + let store = global.0.clone(); + store.update(cx, |store: &mut TerminalSettingsStore, cx| { + let mut next = store.snapshot(); + next.check_running_processes_on_exit = settings.terminal_check_running_processes_on_exit; + store.replace(next, cx); + }); + } + let Some(home) = cx.try_global::() else { return; }; @@ -1293,6 +1310,7 @@ fn legacy_terminal_settings(settings: &AppSettings) -> TerminalSettings { confirm_high_risk_command: settings.terminal_confirm_high_risk_command, builtin_highlights_initialized: false, custom_highlights: Vec::new(), + check_running_processes_on_exit: settings.terminal_check_running_processes_on_exit, } } @@ -2503,6 +2521,27 @@ impl SettingsPanel { .visible_when(|cx| { AppSettings::global(cx).restore_connections_on_startup }), + SettingItem::new( + t!("Settings.General.Terminal.check_running_processes_on_exit"), + SettingField::switch( + |cx: &App| { + AppSettings::global(cx).terminal_check_running_processes_on_exit + }, + |val: bool, cx: &mut App| { + let settings = AppSettings::global_mut(cx); + settings.terminal_check_running_processes_on_exit = val; + settings.save(); + sync_terminal_settings_to_all(settings.clone(), cx); + }, + ) + .default_value( + default_settings.terminal_check_running_processes_on_exit, + ), + ) + .description( + t!("Settings.General.Terminal.check_running_processes_on_exit_desc") + .to_string(), + ), ]), themed_setting_group(SettingGroup::new(), cx) .title(t!("Settings.General.Database.group_title")) @@ -2679,6 +2718,27 @@ impl SettingsPanel { t!("Settings.General.Database.ai_auto_title_desc").to_string(), ), ]), + SettingGroup::new() + .title(t!("Settings.General.Log.group_title")) + .item( + SettingItem::new( + t!("Settings.General.Log.file_path"), + SettingField::input( + |cx: &App| { + SharedString::from( + AppSettings::global(cx).log_file_path.clone(), + ) + }, + |val: SharedString, cx: &mut App| { + let settings = AppSettings::global_mut(cx); + settings.log_file_path = val.trim().to_string(); + settings.save(); + }, + ) + .default_value(SharedString::from("")), + ) + .description(t!("Settings.General.Log.file_path_desc").to_string()), + ), SettingGroup::new() .title(t!("Settings.General.Update.group_title")) .items(vec![ diff --git a/vendor/zed/Cargo.lock b/vendor/zed/Cargo.lock new file mode 100644 index 0000000000..ad209b74cd --- /dev/null +++ b/vendor/zed/Cargo.lock @@ -0,0 +1,7951 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ash-window" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" +dependencies = [ + "ash", + "raw-window-handle", + "raw-window-metal", +] + +[[package]] +name = "ashpd" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.3.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite 2.6.1", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-tar" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "git+https://github.com/smol-rs/async-task.git?rev=b4486cd71e4e94fbda54ce6302444de14f4d190e#b4486cd71e4e94fbda54ce6302444de14f4d190e" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async_zip" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite 2.6.1", + "pin-project", + "thiserror 2.0.18", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "blade-graphics" +version = "0.7.0" +source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" +dependencies = [ + "ash", + "ash-window", + "bitflags 2.11.0", + "bytemuck", + "codespan-reporting", + "glow", + "gpu-alloc", + "gpu-alloc-ash", + "hidden-trait", + "js-sys", + "khronos-egl", + "libloading", + "log", + "mint", + "naga", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "objc2-ui-kit", + "once_cell", + "raw-window-handle", + "slab", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "blade-macros" +version = "0.3.0" +source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "blade-util" +version = "0.3.0" +source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" +dependencies = [ + "blade-graphics", + "bytemuck", + "log", + "profiling", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.14.3" +source = "git+https://github.com/zed-industries/calloop#eb6b4fd17b9af5ecc226546bdd04185391b3e265" +dependencies = [ + "bitflags 2.11.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "circular-buffer" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c638459986b83c2b885179bd4ea6a2cbb05697b001501a56adb3a3d230803b" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.11.0", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "collections" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "indexmap", + "rustc-hash 2.1.1", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "command-fds" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" +dependencies = [ + "nix 0.30.1", + "thiserror 2.0.18", +] + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "deflate64", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" +dependencies = [ + "bitflags 2.11.0", + "block", + "cfg-if", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8c4e3a1d02f5269ed15c2d70b4647167856f66f228dcdf99050ab77bbb5a56" +dependencies = [ + "bitflags 2.11.0", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.1", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix 0.31.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_refineable" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73829a7b5c91198af28a99159b7ae4afbb252fb906159ff7f189f3a2ceaa3df2" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.3.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags 2.11.0", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-ash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a" +dependencies = [ + "ash", + "gpu-alloc-types", + "tinyvec", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "gpui" +version = "0.2.2" +dependencies = [ + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.11.0", + "blade-graphics", + "blade-macros", + "blade-util", + "block", + "bytemuck", + "calloop", + "calloop-wayland-source", + "cbindgen", + "chrono", + "circular-buffer", + "cocoa 0.26.0", + "cocoa-foundation 0.2.0", + "collections", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "core-video", + "cosmic-text", + "ctor", + "derive_more", + "embed-resource", + "env_logger", + "etagere", + "filedescriptor", + "foreign-types", + "futures", + "gpui_macros", + "http_client", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "mach2", + "media", + "metal", + "naga", + "num_cpus", + "objc", + "objc2", + "objc2-metal", + "oo7", + "open", + "parking", + "parking_lot", + "pathfinder_geometry", + "pin-project", + "postage", + "pretty_assertions", + "profiling", + "rand 0.9.2", + "raw-window-handle", + "refineable", + "reqwest_client", + "resvg", + "scheduler", + "schemars", + "seahash", + "semver", + "serde", + "serde_json", + "slotmap", + "smallvec", + "smol", + "spin 0.10.0", + "stacksafe", + "strum 0.27.2", + "sum_tree", + "swash", + "taffy", + "thiserror 2.0.18", + "unicode-segmentation", + "usvg", + "util", + "util_macros", + "uuid", + "waker-fn", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-numerics", + "windows-registry 0.5.3", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-font-kit", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui_macros" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "grid" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hidden-trait" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http_client" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "anyhow", + "async-compression", + "async-fs", + "async-tar", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "sha2", + "tempfile", + "url", + "util", +] + +[[package]] +name = "http_client_tls" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "rustls", + "rustls-platform-verifier", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "inventory" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libz-sys" +version = "1.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_extra", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_extra" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72" +dependencies = [ + "lyon_path", + "thiserror 2.0.18", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "media" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "ctor", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "25.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.15.5", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oo7" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite 2.6.1", + "futures-util", + "getrandom 0.3.4", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand 0.9.2", + "serde", + "sha2", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "perf" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "collections", + "serde", + "serde_json", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.3.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.8+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" +dependencies = [ + "cocoa 0.25.0", + "core-graphics 0.23.2", + "objc", + "raw-window-handle", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "refineable" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "derive_refineable", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest_client" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http_client", + "http_client_tls", + "log", + "regex", + "serde", + "tokio", + "util", + "zed-reqwest", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.117", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.0", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scheduler" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.2", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_json_lenient" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "stacksafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" +dependencies = [ + "stacker", + "stacksafe-macro", +] + +[[package]] +name = "stacksafe-macro" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" +dependencies = [ + "proc-macro-error2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sum_tree" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "arrayvec", + "log", + "rayon", + "tracing", + "ztracing", +] + +[[package]] +name = "sval" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1aaf178a50bbdd86043fce9bf0a5867007d9b382db89d1c96ccae4601ff1ff9" + +[[package]] +name = "sval_buffer" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89273e48f03807ebf51c4d81c52f28d35ffa18a593edf97e041b52de143df89" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0430f4e18e7eba21a49d10d25a8dec3ce0e044af40b162347e99a8e3c3ced864" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835f51b9d7331b9d7fc48fc716c02306fa88c4a076b1573531910c91a525882d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13cbfe3ef406ee2366e7e8ab3678426362085fa9eaedf28cb878a967159dced3" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20358af4af787c34321a86618c3cae12eabdd0e9df22cd9dd2c6834214c518" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5e500f8eb2efa84f75e7090f7fc43f621b9f8b6cde571c635b3855f97b332a" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2032ae39b11dcc6c18d5fbc50a661ea191cac96484c59ccf49b002261ca2c1" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +dependencies = [ + "skrifa 0.37.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "take-until" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +dependencies = [ + "indexmap", + "toml_datetime 1.1.0+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a559e63b5d8004e12f9bce88af5c6d939c58de839b7532cfe9653846cedd2a9e" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "util" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "anyhow", + "async-fs", + "async_zip", + "collections", + "command-fds", + "dirs 4.0.0", + "dunce", + "futures", + "futures-lite 1.13.0", + "git2", + "globset", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "nix 0.29.0", + "percent-encoding", + "rand 0.9.2", + "regex", + "rust-embed", + "schemars", + "serde", + "serde_json", + "serde_json_lenient", + "shlex", + "smol", + "take-until", + "tempfile", + "tendril", + "unicase", + "url", + "util_macros", + "walkdir", + "which", +] + +[[package]] +name = "util_macros" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "perf", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.6", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-capture" +version = "1.4.3" +source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" +dependencies = [ + "clap", + "ctrlc", + "parking_lot", + "rayon", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-future", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix 1.1.4", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.1" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite 2.6.1", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "git+https://github.com/zed-industries/font-kit?rev=110523127440aefb11ce0cf280ae7c5071337ec5#110523127440aefb11ce0cf280ae7c5071337ec5" +dependencies = [ + "bitflags 2.11.0", + "byteorder", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-reqwest" +version = "0.12.15-zed" +source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "git+https://github.com/zed-industries/scap?rev=4afea48c3b002197176fb19cd0f9b180dd36eaac#4afea48c3b002197176fb19cd0f9b180dd36eaac" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.5", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zlog" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "anyhow", + "chrono", + "collections", + "log", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "ztracing" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" +dependencies = [ + "tracing", + "tracing-subscriber", + "zlog", + "ztracing_macro", +] + +[[package]] +name = "ztracing_macro" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed?rev=8b5328cad3d9ca3576296be574a1c7bfbcaf3557#8b5328cad3d9ca3576296be574a1c7bfbcaf3557" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] + +[[patch.unused]] +name = "notify" +version = "8.2.0" +source = "git+https://github.com/zed-industries/notify.git?rev=6c550ac3c56cbd143c57ea6390e197af9d790908#6c550ac3c56cbd143c57ea6390e197af9d790908" + +[[patch.unused]] +name = "notify-types" +version = "2.0.0" +source = "git+https://github.com/zed-industries/notify.git?rev=6c550ac3c56cbd143c57ea6390e197af9d790908#6c550ac3c56cbd143c57ea6390e197af9d790908"