diff --git a/Cargo.lock b/Cargo.lock index 8f36f2445d..c853c0a0ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9925,7 +9925,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0" dependencies = [ "cc", "regex", @@ -11493,7 +11493,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.114.0" +version = "0.114.4" dependencies = [ "activity_indicator", "ai", diff --git a/Cargo.toml b/Cargo.toml index 8840d912f0..c67a4b10e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,8 +195,9 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} + [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index ef6a655bdc..2a8d19f882 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -530,12 +530,17 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "enter": "project_panel::Rename", - "space": "project_panel::Open", "backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" } }, + { + "context": "ProjectPanel && not_editing", + "bindings": { + "space": "project_panel::Open" + } + }, { "context": "CollabPanel && not_editing", "bindings": { diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 1f11e0650d..14cb28c32d 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -464,7 +464,7 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { if let Some(room) = self.room() { let room = room.read(cx); report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); @@ -477,7 +477,7 @@ pub fn report_call_event_for_room( room_id: u64, channel_id: Option, client: &Arc, - cx: &AppContext, + cx: &mut AppContext, ) { let telemetry = client.telemetry(); let telemetry_settings = *TelemetrySettings::get_global(cx); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8f7fbeb83d..a3e7449cf8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -109,6 +109,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -168,13 +172,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|mut cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -256,7 +255,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -273,7 +272,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -290,7 +289,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -307,7 +306,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -322,7 +321,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -337,7 +336,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -358,6 +371,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -370,7 +384,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index b4279b023e..4ad354f2f9 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings { } impl Client { - pub fn new(http: Arc, cx: &AppContext) -> Arc { + pub fn new(http: Arc, cx: &mut AppContext) -> Arc { Arc::new(Self { id: AtomicU64::new(0), peer: Peer::new(0), diff --git a/crates/client2/src/telemetry.rs b/crates/client2/src/telemetry.rs index 9c88d1102c..b303e68183 100644 --- a/crates/client2/src/telemetry.rs +++ b/crates/client2/src/telemetry.rs @@ -1,5 +1,6 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use chrono::{DateTime, Utc}; +use futures::Future; use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -107,6 +108,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); impl Telemetry { - pub fn new(client: Arc, cx: &AppContext) -> Arc { + pub fn new(client: Arc, cx: &mut AppContext) -> Arc { let release_channel = if cx.has_global::() { Some(cx.global::().display_name()) } else { None }; + // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, @@ -147,9 +153,30 @@ impl Telemetry { }), }); + // We should only ever have one instance of Telemetry, leak the subscription to keep it alive + // rather than store in TelemetryState, complicating spawn as subscriptions are not Send + std::mem::forget(cx.on_app_quit({ + let this = this.clone(); + move |cx| this.shutdown_telemetry(cx) + })); + this } + #[cfg(any(test, feature = "test-support"))] + fn shutdown_telemetry(self: &Arc, _: &mut AppContext) -> impl Future { + Task::ready(()) + } + + // Skip calling this function in tests. + // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings + #[cfg(not(any(test, feature = "test-support")))] + fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); + self.report_app_event(telemetry_settings, "close"); + Task::ready(()) + } + pub fn log_file_path(&self) -> Option { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } @@ -163,13 +190,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -257,7 +279,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -274,7 +296,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -291,7 +313,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -308,7 +330,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -323,7 +345,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -338,7 +360,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -359,6 +395,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -371,7 +408,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 090a32d4ca..6bb57e11ab 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -149,7 +149,7 @@ impl TestServer { .user_id }; let client_name = name.to_string(); - let mut client = cx.read(|cx| Client::new(http.clone(), cx)); + let mut client = cx.update(|cx| Client::new(http.clone(), cx)); let server = self.server.clone(); let db = self.app_state.db.clone(); let connection_killers = self.connection_killers.clone(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2558aec121..17712b7e78 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1001,17 +1001,18 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, + editor: &Editor, cx: &mut ViewContext, - ) { + ) -> Option> { let settings = settings::get::(cx); if !settings.show_completion_documentation { - return; + return None; } - let Some(project) = project else { - return; + let Some(project) = editor.project.clone() else { + return None; }; + let client = project.read(cx).client(); let language_registry = project.read(cx).languages().clone(); @@ -1021,7 +1022,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - cx.spawn(move |this, mut cx| async move { + Some(cx.spawn(move |this, mut cx| async move { if is_remote { let Some(project_id) = project_id else { log::error!("Remote project without remote_id"); @@ -1083,8 +1084,7 @@ impl CompletionsMenu { _ = this.update(&mut cx, |_, cx| cx.notify()); } } - }) - .detach(); + })) } fn attempt_resolve_selected_completion_documentation( @@ -3580,7 +3580,8 @@ impl Editor { let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { async move { - let menu = if let Some(completions) = completions.await.log_err() { + let completions = completions.await.log_err(); + let (menu, pre_resolve_task) = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3601,21 +3602,26 @@ impl Editor { selected_item: 0, list: Default::default(), }; + menu.filter(query.as_deref(), cx.background()).await; + if menu.matches.is_empty() { - None + (None, None) } else { - _ = this.update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor.project.clone(), cx); - }); - Some(menu) + let pre_resolve_task = this + .update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor, cx) + }) + .ok() + .flatten(); + (Some(menu), pre_resolve_task) } } else { - None + (None, None) }; this.update(&mut cx, |this, cx| { - this.completion_tasks.retain(|(task_id, _)| *task_id > id); + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); let mut context_menu = this.context_menu.write(); match context_menu.as_ref() { @@ -3636,10 +3642,10 @@ impl Editor { drop(context_menu); this.discard_copilot_suggestion(cx); cx.notify(); - } else if this.completion_tasks.is_empty() { - // If there are no more completion tasks and the last menu was - // empty, we should hide it. If it was already hidden, we should - // also show the copilot suggestion when available. + } else if this.completion_tasks.len() <= 1 { + // If there are no more completion tasks (omitting ourself) and + // the last menu was empty, we should hide it. If it was already + // hidden, we should also show the copilot suggestion when available. drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); @@ -3647,10 +3653,15 @@ impl Editor { } })?; + if let Some(pre_resolve_task) = pre_resolve_task { + pre_resolve_task.await; + } + Ok::<_, anyhow::Error>(()) } .log_err() }); + self.completion_tasks.push((id, task)); } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index f645129706..617c0b5600 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -10,6 +10,7 @@ pub use entity_map::*; pub use model_context::*; use refineable::Refineable; use smallvec::SmallVec; +use smol::future::FutureExt; #[cfg(any(test, feature = "test-support"))] pub use test_context::*; @@ -983,6 +984,22 @@ impl AppContext { pub fn all_action_names(&self) -> &[SharedString] { self.actions.all_action_names() } + + pub fn on_app_quit( + &mut self, + mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + async move { future.await }.boxed_local() + }), + ) + } } impl Context for AppContext { diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 109d79cf70..cf790e803e 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -98,9 +98,9 @@ mod tests { ); let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1d22d7773b..811e549406 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -197,8 +197,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -312,7 +316,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } @@ -1383,7 +1387,7 @@ impl Language { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); - for (ix, name) in query.capture_names().iter().enumerate() { + for (ix, name) in query.capture_names().iter().copied().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { @@ -1396,7 +1400,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.into(), value)); } } diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index bd50608122..f20f481613 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index 1421ef672d..8e7a35233c 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -100,9 +100,9 @@ mod tests { }; let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 311049f032..8fdf524f69 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -200,8 +200,12 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { - self.adapter.workspace_configuration(cx) + pub fn workspace_configuration( + &self, + workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { + self.adapter.workspace_configuration(workspace_root, cx) } pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -315,7 +319,7 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> { futures::future::ready(serde_json::json!({})).boxed() } @@ -1391,7 +1395,7 @@ impl Language { let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { - let value = self.config.overrides.remove(name).unwrap_or_default(); + let value = self.config.overrides.remove(*name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { if !self .config @@ -1402,7 +1406,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.to_string(), value)); } } diff --git a/crates/language2/src/syntax_map/syntax_map_tests.rs b/crates/language2/src/syntax_map/syntax_map_tests.rs index bd50608122..f20f481613 100644 --- a/crates/language2/src/syntax_map/syntax_map_tests.rs +++ b/crates/language2/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 98fd81f012..dc5b63d222 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -429,8 +429,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -451,12 +451,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -501,7 +504,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -515,6 +518,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -524,15 +541,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/lsp2/src/lsp2.rs b/crates/lsp2/src/lsp2.rs index 356d029c58..788c424373 100644 --- a/crates/lsp2/src/lsp2.rs +++ b/crates/lsp2/src/lsp2.rs @@ -434,8 +434,8 @@ impl LanguageServer { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { - process_id: Default::default(), - root_path: Default::default(), + process_id: None, + root_path: None, root_uri: Some(root_uri.clone()), initialization_options: options, capabilities: ClientCapabilities { @@ -456,12 +456,15 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), + diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + refresh_support: None, + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - ..Default::default() + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -503,7 +506,7 @@ impl LanguageServer { }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - ..Default::default() + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -517,6 +520,20 @@ impl LanguageServer { }), dynamic_registration: Some(false), }), + publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { + related_information: Some(true), + ..Default::default() + }), + formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + on_type_formatting: Some(DynamicRegistrationClientCapabilities { + dynamic_registration: None, + }), + diagnostic: Some(DiagnosticClientCapabilities { + related_document_support: Some(true), + dynamic_registration: None, + }), ..Default::default() }), experimental: Some(json!({ @@ -526,15 +543,15 @@ impl LanguageServer { work_done_progress: Some(true), ..Default::default() }), - ..Default::default() + general: None, }, - trace: Default::default(), + trace: None, workspace_folders: Some(vec![WorkspaceFolder { uri: root_uri, name: Default::default(), }]), - client_info: Default::default(), - locale: Default::default(), + client_info: None, + locale: None, }; let response = self.request::(params).await?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c24fb5eea1..7769932235 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2629,8 +2629,9 @@ impl Project { }); for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx)) + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2738,7 +2739,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2761,6 +2762,7 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let result = Self::setup_and_insert_language_server( this, + &worktree_path, override_options, pending_server, adapter.clone(), @@ -2876,6 +2878,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModelHandle, + worktree_path: &Path, override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2888,6 +2891,7 @@ impl Project { this, override_initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2917,11 +2921,14 @@ impl Project { this: WeakModelHandle, override_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx)) + .await; let language_server = pending_server.task.await?; language_server @@ -2949,11 +2956,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, mut cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx)).await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx)) + .await; Ok(params .items .into_iter() diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82fa5d6020..ac494f87d9 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2226,7 +2226,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + fn is_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) @@ -2399,26 +2399,9 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn reload_repositories(&mut self, changed_paths: &[Arc], fs: &dyn Fs) { + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet, fs: &dyn Fs) { let scan_id = self.snapshot.scan_id; - - // Find each of the .git directories that contain any of the given paths. - let mut prev_dot_git_dir = None; - for changed_path in changed_paths { - let Some(dot_git_dir) = changed_path - .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) - else { - continue; - }; - - // Avoid processing the same repository multiple times, if multiple paths - // within it have changed. - if prev_dot_git_dir == Some(dot_git_dir) { - continue; - } - prev_dot_git_dir = Some(dot_git_dir); - + for dot_git_dir in dot_git_dirs_to_reload { // If there is already a repository for this .git directory, reload // the status for all of its files. let repository = self @@ -2430,7 +2413,7 @@ impl BackgroundScannerState { }); match repository { None => { - self.build_git_repository(dot_git_dir.into(), fs); + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); } Some((entry_id, repository)) => { if repository.git_dir_scan_id == scan_id { @@ -2444,7 +2427,7 @@ impl BackgroundScannerState { continue; }; - log::info!("reload git repository {:?}", dot_git_dir); + log::info!("reload git repository {dot_git_dir:?}"); let repository = repository.repo_ptr.lock(); let branch = repository.branch_name(); repository.reload_index(); @@ -2475,7 +2458,9 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - if snapshot.is_abs_path_excluded(&git_dir_abs_path) + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) + || snapshot.is_path_excluded(&git_dir_abs_path); + if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { ids_to_preserve.insert(work_directory_id); @@ -3314,11 +3299,26 @@ impl BackgroundScanner { }; let mut relative_paths = Vec::with_capacity(abs_paths.len()); + let mut dot_git_paths_to_reload = HashSet::default(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.retain(|abs_path| { let snapshot = &self.state.lock().snapshot; { + let mut is_git_related = false; + if let Some(dot_git_dir) = abs_path + .ancestors() + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + { + let dot_git_path = dot_git_dir + .strip_prefix(&root_canonical_path) + .ok() + .map(|path| path.to_path_buf()) + .unwrap_or_else(|| dot_git_dir.to_path_buf()); + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); + is_git_related = true; + } + let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { path.into() @@ -3328,23 +3328,30 @@ impl BackgroundScanner { ); return false; }; + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + snapshot + .entry_for_path(parent) + .map_or(false, |entry| entry.kind == EntryKind::Dir) + }); + if !parent_dir_is_loaded { + log::debug!("ignoring event {relative_path:?} within unloaded directory"); + return false; + } - if !is_git_related(&abs_path) { - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { - snapshot - .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - return false; + // FS events may come for files which parent directory is excluded, need to check ignore those. + let mut path_to_test = abs_path.clone(); + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) + || snapshot.is_path_excluded(&relative_path); + while !excluded_file_event && path_to_test.pop() { + if snapshot.is_path_excluded(&path_to_test) { + excluded_file_event = true; } - if snapshot.is_abs_path_excluded(abs_path) { - log::debug!( - "ignoring FS event for path {relative_path:?} within excluded directory" - ); - return false; + } + if excluded_file_event { + if !is_git_related { + log::debug!("ignoring FS event for excluded path {relative_path:?}"); } + return false; } relative_paths.push(relative_path); @@ -3352,31 +3359,39 @@ impl BackgroundScanner { } }); - if relative_paths.is_empty() { + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { return; } - log::debug!("received fs events {:?}", relative_paths); + if !relative_paths.is_empty() { + log::debug!("received fs events {:?}", relative_paths); - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.reload_entries_for_paths( - root_path, - root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - drop(scan_job_tx); - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &relative_paths, + abs_paths, + Some(scan_job_tx.clone()), + ) + .await; + drop(scan_job_tx); + self.scan_dirs(false, scan_job_rx).await; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses(scan_job_tx).await; + self.scan_dirs(false, scan_job_rx).await; + } { let mut state = self.state.lock(); - state.reload_repositories(&relative_paths, self.fs.as_ref()); + if !dot_git_paths_to_reload.is_empty() { + if relative_paths.is_empty() { + state.snapshot.scan_id += 1; + } + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); + } state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { state.scanned_dirs.remove(&entry_id); @@ -3516,7 +3531,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_abs_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(&job.abs_path) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3588,7 +3603,7 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_abs_path_excluded(&child_abs_path) { + if state.snapshot.is_path_excluded(&child_abs_path) { let relative_path = job.path.join(child_name); log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); @@ -4130,12 +4145,6 @@ impl BackgroundScanner { } } -fn is_git_related(abs_path: &Path) -> bool { - abs_path - .components() - .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 22a5cc1e01..b4cf162d8f 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -990,6 +990,145 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { + init_test(cx); + let dir = temp_tree(json!({ + ".git": { + "HEAD": "ref: refs/heads/main\n", + "foo": "bar", + }, + ".gitignore": "**/target\n/node_modules\ntest_output\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![ + "**/.git".to_string(), + "node_modules/".to_string(), + "build_output".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + build_client(cx), + dir.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + ], + &["target", "node_modules"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + ".gitignore", + ], + ) + }); + + let new_excluded_dir = dir.path().join("build_output"); + let new_ignored_dir = dir.path().join("test_output"); + std::fs::create_dir_all(&new_excluded_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); + std::fs::create_dir_all(&new_ignored_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); + let node_modules_dir = dir.path().join("node_modules"); + let dot_git_dir = dir.path().join(".git"); + let src_dir = dir.path().join("src"); + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { + assert!( + existing_dir.is_dir(), + "Expect {existing_dir:?} to be present in the FS already" + ); + } + + for directory_for_new_file in [ + new_excluded_dir, + new_ignored_dir, + node_modules_dir, + dot_git_dir, + src_dir, + ] { + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") + .unwrap_or_else(|e| { + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") + }); + } + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + ".git/new_file", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + "node_modules/new_file", + "build_output", + "build_output/new_file", + "test_output/new_file", + ], + &["target", "node_modules", "test_output"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + "src/new_file", + ".gitignore", + ], + ) + }); +} + #[gpui::test(iterations = 30)] async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 3f7c9b7188..735eea6d75 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -2667,8 +2667,9 @@ impl Project { })?; for (adapter, server) in servers { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(server.root_path(), cx))? + .await; server .notify::( lsp::DidChangeConfigurationParams { @@ -2777,7 +2778,7 @@ impl Project { stderr_capture.clone(), language.clone(), adapter.clone(), - worktree_path, + Arc::clone(&worktree_path), ProjectLspAdapterDelegate::new(self, cx), cx, ) { @@ -2809,6 +2810,7 @@ impl Project { cx.spawn(move |this, mut cx| async move { let result = Self::setup_and_insert_language_server( this.clone(), + &worktree_path, initialization_options, pending_server, adapter.clone(), @@ -2929,6 +2931,7 @@ impl Project { async fn setup_and_insert_language_server( this: WeakModel, + worktree_path: &Path, initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, @@ -2941,6 +2944,7 @@ impl Project { this.clone(), initialization_options, pending_server, + worktree_path, adapter.clone(), server_id, cx, @@ -2970,11 +2974,14 @@ impl Project { this: WeakModel, initialization_options: Option, pending_server: PendingLanguageServer, + worktree_path: &Path, adapter: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result> { - let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(worktree_path, cx))? + .await; let language_server = pending_server.task.await?; language_server @@ -3003,11 +3010,14 @@ impl Project { language_server .on_request::({ let adapter = adapter.clone(); + let worktree_path = worktree_path.to_path_buf(); move |params, cx| { let adapter = adapter.clone(); + let worktree_path = worktree_path.clone(); async move { - let workspace_config = - cx.update(|cx| adapter.workspace_configuration(cx))?.await; + let workspace_config = cx + .update(|cx| adapter.workspace_configuration(&worktree_path, cx))? + .await; Ok(params .items .into_iter() diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index fcb64c40b4..190a8f4672 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -2222,7 +2222,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + fn is_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) @@ -2395,26 +2395,10 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn reload_repositories(&mut self, changed_paths: &[Arc], fs: &dyn Fs) { + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet, fs: &dyn Fs) { let scan_id = self.snapshot.scan_id; - // Find each of the .git directories that contain any of the given paths. - let mut prev_dot_git_dir = None; - for changed_path in changed_paths { - let Some(dot_git_dir) = changed_path - .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) - else { - continue; - }; - - // Avoid processing the same repository multiple times, if multiple paths - // within it have changed. - if prev_dot_git_dir == Some(dot_git_dir) { - continue; - } - prev_dot_git_dir = Some(dot_git_dir); - + for dot_git_dir in dot_git_dirs_to_reload { // If there is already a repository for this .git directory, reload // the status for all of its files. let repository = self @@ -2426,7 +2410,7 @@ impl BackgroundScannerState { }); match repository { None => { - self.build_git_repository(dot_git_dir.into(), fs); + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); } Some((entry_id, repository)) => { if repository.git_dir_scan_id == scan_id { @@ -2440,7 +2424,7 @@ impl BackgroundScannerState { continue; }; - log::info!("reload git repository {:?}", dot_git_dir); + log::info!("reload git repository {dot_git_dir:?}"); let repository = repository.repo_ptr.lock(); let branch = repository.branch_name(); repository.reload_index(); @@ -2471,7 +2455,9 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - if snapshot.is_abs_path_excluded(&git_dir_abs_path) + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) + || snapshot.is_path_excluded(&git_dir_abs_path); + if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { ids_to_preserve.insert(work_directory_id); @@ -3303,11 +3289,26 @@ impl BackgroundScanner { }; let mut relative_paths = Vec::with_capacity(abs_paths.len()); + let mut dot_git_paths_to_reload = HashSet::default(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.retain(|abs_path| { let snapshot = &self.state.lock().snapshot; { + let mut is_git_related = false; + if let Some(dot_git_dir) = abs_path + .ancestors() + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + { + let dot_git_path = dot_git_dir + .strip_prefix(&root_canonical_path) + .ok() + .map(|path| path.to_path_buf()) + .unwrap_or_else(|| dot_git_dir.to_path_buf()); + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); + is_git_related = true; + } + let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { path.into() @@ -3318,22 +3319,30 @@ impl BackgroundScanner { return false; }; - if !is_git_related(&abs_path) { - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { - snapshot - .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - return false; + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + snapshot + .entry_for_path(parent) + .map_or(false, |entry| entry.kind == EntryKind::Dir) + }); + if !parent_dir_is_loaded { + log::debug!("ignoring event {relative_path:?} within unloaded directory"); + return false; + } + + // FS events may come for files which parent directory is excluded, need to check ignore those. + let mut path_to_test = abs_path.clone(); + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) + || snapshot.is_path_excluded(&relative_path); + while !excluded_file_event && path_to_test.pop() { + if snapshot.is_path_excluded(&path_to_test) { + excluded_file_event = true; } - if snapshot.is_abs_path_excluded(abs_path) { - log::debug!( - "ignoring FS event for path {relative_path:?} within excluded directory" - ); - return false; + } + if excluded_file_event { + if !is_git_related { + log::debug!("ignoring FS event for excluded path {relative_path:?}"); } + return false; } relative_paths.push(relative_path); @@ -3341,31 +3350,39 @@ impl BackgroundScanner { } }); - if relative_paths.is_empty() { + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { return; } - log::debug!("received fs events {:?}", relative_paths); + if !relative_paths.is_empty() { + log::debug!("received fs events {:?}", relative_paths); - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.reload_entries_for_paths( - root_path, - root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - drop(scan_job_tx); - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &relative_paths, + abs_paths, + Some(scan_job_tx.clone()), + ) + .await; + drop(scan_job_tx); + self.scan_dirs(false, scan_job_rx).await; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses(scan_job_tx).await; + self.scan_dirs(false, scan_job_rx).await; + } { let mut state = self.state.lock(); - state.reload_repositories(&relative_paths, self.fs.as_ref()); + if !dot_git_paths_to_reload.is_empty() { + if relative_paths.is_empty() { + state.snapshot.scan_id += 1; + } + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); + } state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { state.scanned_dirs.remove(&entry_id); @@ -3505,7 +3522,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_abs_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(&job.abs_path) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3577,7 +3594,7 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_abs_path_excluded(&child_abs_path) { + if state.snapshot.is_path_excluded(&child_abs_path) { let relative_path = job.path.join(child_name); log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); @@ -4119,12 +4136,6 @@ impl BackgroundScanner { } } -fn is_git_related(abs_path: &Path) -> bool { - abs_path - .components() - .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs index df7307f694..501a5f736f 100644 --- a/crates/project2/src/worktree_tests.rs +++ b/crates/project2/src/worktree_tests.rs @@ -992,6 +992,146 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".git": { + "HEAD": "ref: refs/heads/main\n", + "foo": "bar", + }, + ".gitignore": "**/target\n/node_modules\ntest_output\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![ + "**/.git".to_string(), + "node_modules/".to_string(), + "build_output".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + build_client(cx), + dir.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + ], + &["target", "node_modules"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + ".gitignore", + ], + ) + }); + + let new_excluded_dir = dir.path().join("build_output"); + let new_ignored_dir = dir.path().join("test_output"); + std::fs::create_dir_all(&new_excluded_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); + std::fs::create_dir_all(&new_ignored_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); + let node_modules_dir = dir.path().join("node_modules"); + let dot_git_dir = dir.path().join(".git"); + let src_dir = dir.path().join("src"); + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { + assert!( + existing_dir.is_dir(), + "Expect {existing_dir:?} to be present in the FS already" + ); + } + + for directory_for_new_file in [ + new_excluded_dir, + new_ignored_dir, + node_modules_dir, + dot_git_dir, + src_dir, + ] { + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") + .unwrap_or_else(|e| { + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") + }); + } + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + ".git/new_file", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + "node_modules/new_file", + "build_output", + "build_output/new_file", + "test_output/new_file", + ], + &["target", "node_modules", "test_output"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + "src/new_file", + ".gitignore", + ], + ) + }); +} + #[gpui::test(iterations = 30)] async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { init_test(cx); @@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_fake = FakeFs::new(cx.background_executor.clone()); fs_fake @@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); }); - let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_real = Arc::new(RealFs); let temp_root = temp_tree(json!({ @@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { fn build_client(cx: &mut TestAppContext) -> Arc { let http_client = FakeHttpClient::with_404_response(); - cx.read(|cx| Client::new(http_client, cx)) + cx.update(|cx| Client::new(http_client, cx)) } #[track_caller] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index eb124bfca2..553303e90d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1627,9 +1627,17 @@ impl View for ProjectPanel { } } - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { Self::reset_to_default_keymap_context(keymap); keymap.add_identifier("menu"); + + if let Some(window) = cx.active_window() { + window.read_with(cx, |cx| { + if !self.filename_editor.is_focused(cx) { + keymap.add_identifier("not_editing"); + }; + }); + } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 2145d1f9e0..f4e2c5ea13 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc { target: (identifier) @name) operator: "when") ]) - (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item ) (call target: (identifier) @name (arguments (alias) @name) - (#match? @name "^(defmodule|defprotocol)$")) @item + (#any-match? @name "^(defmodule|defprotocol)$")) @item "#, ) .unwrap(), diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ab8d5b7efe..d78741bfe1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.114.0" +version = "0.114.4" publish = false [lib] diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 90012116c0..870bbe4e50 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -dev \ No newline at end of file +stable \ No newline at end of file diff --git a/crates/zed/src/languages/elixir/embedding.scm b/crates/zed/src/languages/elixir/embedding.scm index 16ad20746d..743ebe4d2f 100644 --- a/crates/zed/src/languages/elixir/embedding.scm +++ b/crates/zed/src/languages/elixir/embedding.scm @@ -18,10 +18,10 @@ target: (identifier) @name) operator: "when") ]) - (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item ) (call target: (identifier) @name (arguments (alias) @name) - (#match? @name "^(defmodule|defprotocol)$")) @item + (#any-match? @name "^(defmodule|defprotocol)$")) @item diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 63f909ae2a..891c25c31f 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter { fn workspace_configuration( &self, + _workspace_root: &Path, cx: &mut AppContext, ) -> BoxFuture<'static, serde_json::Value> { let action_names = cx.all_action_names().collect::>(); diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs index 3096fd16e6..e3d0f1c690 100644 --- a/crates/zed/src/languages/php.rs +++ b/crates/zed/src/languages/php.rs @@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter { impl IntelephenseLspAdapter { const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; - #[allow(unused)] pub fn new(node: Arc) -> Self { Self { node } } diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs index 6d6006dbd4..0dfa700b01 100644 --- a/crates/zed/src/languages/tailwind.rs +++ b/crates/zed/src/languages/tailwind.rs @@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter { })) } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + _workspace_root: &Path, + _: &mut AppContext, + ) -> BoxFuture<'static, Value> { future::ready(json!({ "tailwindCSS": { "emmetCompletions": true, diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index d259afb05d..fbb14930fc 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -205,7 +205,6 @@ pub struct EsLintLspAdapter { impl EsLintLspAdapter { const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; - #[allow(unused)] pub fn new(node: Arc) -> Self { EsLintLspAdapter { node } } @@ -213,13 +212,23 @@ impl EsLintLspAdapter { #[async_trait] impl LspAdapter for EsLintLspAdapter { - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + workspace_root: &Path, + _: &mut AppContext, + ) -> BoxFuture<'static, Value> { future::ready(json!({ "": { "validate": "on", "rulesCustomizations": [], "run": "onType", "nodePath": null, + "workingDirectory": {"mode": "auto"}, + "workspaceFolder": { + "uri": workspace_root, + "name": workspace_root.file_name() + .unwrap_or_else(|| workspace_root.as_os_str()), + }, } })) .boxed() diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 8b438d0949..fbed9ba78f 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &*self.node).await } - fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + _workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { let tab_size = all_language_settings(None, cx) .language(Some("YAML")) .tab_size; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5f2a7c525e..20b93ae6bb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -65,7 +65,8 @@ fn main() { log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); - let installation_id = app.background().block(installation_id()).ok(); + let (installation_id, existing_installation_id_found) = + app.background().block(installation_id()).ok().unzip(); let session_id = Uuid::new_v4().to_string(); init_panic_hook(&app, installation_id.clone(), session_id.clone()); @@ -166,6 +167,14 @@ fn main() { .detach(); client.telemetry().start(installation_id, session_id, cx); + let telemetry_settings = *settings::get::(cx); + let event_operation = match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }; + client + .telemetry() + .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { languages, @@ -317,11 +326,11 @@ async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { Ok::<_, anyhow::Error>(()) } -async fn installation_id() -> Result { +async fn installation_id() -> Result<(String, bool)> { let legacy_key_name = "device_id"; if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { - Ok(installation_id) + Ok((installation_id, true)) } else { let installation_id = Uuid::new_v4().to_string(); @@ -329,7 +338,7 @@ async fn installation_id() -> Result { .write_kvp(legacy_key_name.to_string(), installation_id.clone()) .await?; - Ok(installation_id) + Ok((installation_id, false)) } } diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs index f04f59cf6d..162d4c9fdb 100644 --- a/crates/zed2/src/languages/json.rs +++ b/crates/zed2/src/languages/json.rs @@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter { fn workspace_configuration( &self, + _workspace_root: &Path, cx: &mut AppContext, ) -> BoxFuture<'static, serde_json::Value> { let action_names = cx.all_action_names(); diff --git a/crates/zed2/src/languages/php.rs b/crates/zed2/src/languages/php.rs index 3096fd16e6..e3d0f1c690 100644 --- a/crates/zed2/src/languages/php.rs +++ b/crates/zed2/src/languages/php.rs @@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter { impl IntelephenseLspAdapter { const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; - #[allow(unused)] pub fn new(node: Arc) -> Self { Self { node } } diff --git a/crates/zed2/src/languages/tailwind.rs b/crates/zed2/src/languages/tailwind.rs index 6d6006dbd4..0dfa700b01 100644 --- a/crates/zed2/src/languages/tailwind.rs +++ b/crates/zed2/src/languages/tailwind.rs @@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter { })) } - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + _workspace_root: &Path, + _: &mut AppContext, + ) -> BoxFuture<'static, Value> { future::ready(json!({ "tailwindCSS": { "emmetCompletions": true, diff --git a/crates/zed2/src/languages/typescript.rs b/crates/zed2/src/languages/typescript.rs index 1b96e7e702..26ad337920 100644 --- a/crates/zed2/src/languages/typescript.rs +++ b/crates/zed2/src/languages/typescript.rs @@ -205,7 +205,6 @@ pub struct EsLintLspAdapter { impl EsLintLspAdapter { const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js"; - #[allow(unused)] pub fn new(node: Arc) -> Self { EsLintLspAdapter { node } } @@ -213,13 +212,23 @@ impl EsLintLspAdapter { #[async_trait] impl LspAdapter for EsLintLspAdapter { - fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + workspace_root: &Path, + _: &mut AppContext, + ) -> BoxFuture<'static, Value> { future::ready(json!({ "": { "validate": "on", "rulesCustomizations": [], "run": "onType", "nodePath": null, + "workingDirectory": {"mode": "auto"}, + "workspaceFolder": { + "uri": workspace_root, + "name": workspace_root.file_name() + .unwrap_or_else(|| workspace_root.as_os_str()), + }, } })) .boxed() diff --git a/crates/zed2/src/languages/yaml.rs b/crates/zed2/src/languages/yaml.rs index 8b438d0949..fbed9ba78f 100644 --- a/crates/zed2/src/languages/yaml.rs +++ b/crates/zed2/src/languages/yaml.rs @@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &*self.node).await } - fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { + fn workspace_configuration( + &self, + _workspace_root: &Path, + cx: &mut AppContext, + ) -> BoxFuture<'static, Value> { let tab_size = all_language_settings(None, cx) .language(Some("YAML")) .tab_size; diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index c6737628a9..46be582129 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -71,7 +71,11 @@ fn main() { log::info!("========== starting zed =========="); let app = App::production(Arc::new(Assets)); - let installation_id = app.background_executor().block(installation_id()).ok(); + let (installation_id, existing_installation_id_found) = app + .background_executor() + .block(installation_id()) + .ok() + .unzip(); let session_id = Uuid::new_v4().to_string(); init_panic_hook(&app, installation_id.clone(), session_id.clone()); @@ -173,6 +177,14 @@ fn main() { // .detach(); client.telemetry().start(installation_id, session_id, cx); + let telemetry_settings = *client::TelemetrySettings::get_global(cx); + let event_operation = match existing_installation_id_found { + Some(false) => "first open", + _ => "open", + }; + client + .telemetry() + .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { languages, @@ -333,11 +345,11 @@ fn main() { // Ok::<_, anyhow::Error>(()) // } -async fn installation_id() -> Result { +async fn installation_id() -> Result<(String, bool)> { let legacy_key_name = "device_id"; if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { - Ok(installation_id) + Ok((installation_id, true)) } else { let installation_id = Uuid::new_v4().to_string(); @@ -345,7 +357,7 @@ async fn installation_id() -> Result { .write_kvp(legacy_key_name.to_string(), installation_id.clone()) .await?; - Ok(installation_id) + Ok((installation_id, false)) } }