From 0de4a93ec78bacaef4351b2091b52c0f55953b04 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 23 Oct 2023 11:43:05 +0200 Subject: [PATCH] WIP --- Cargo.lock | 55 + Cargo.toml | 1 + crates/gpui2/src/app.rs | 31 +- crates/gpui2/src/app/model_context.rs | 23 + crates/gpui2/src/executor.rs | 9 +- crates/language2/src/buffer.rs | 6 +- crates/language2/src/syntax_map.rs | 2 + crates/project2/Cargo.toml | 84 + crates/project2/src/ignore.rs | 57 + crates/project2/src/lsp_command.rs | 2352 ++++++ crates/project2/src/project2.rs | 8846 +++++++++++++++++++++++ crates/project2/src/project_settings.rs | 47 + crates/project2/src/project_tests.rs | 4077 +++++++++++ crates/project2/src/search.rs | 458 ++ crates/project2/src/terminals.rs | 124 + crates/project2/src/worktree.rs | 4387 +++++++++++ crates/project2/src/worktree_tests.rs | 2141 ++++++ 17 files changed, 22689 insertions(+), 11 deletions(-) create mode 100644 crates/project2/Cargo.toml create mode 100644 crates/project2/src/ignore.rs create mode 100644 crates/project2/src/lsp_command.rs create mode 100644 crates/project2/src/project2.rs create mode 100644 crates/project2/src/project_settings.rs create mode 100644 crates/project2/src/project_tests.rs create mode 100644 crates/project2/src/search.rs create mode 100644 crates/project2/src/terminals.rs create mode 100644 crates/project2/src/worktree.rs create mode 100644 crates/project2/src/worktree_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 81489e2890..c392f067c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5927,6 +5927,61 @@ dependencies = [ "util", ] +[[package]] +name = "project2" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "async-trait", + "backtrace", + "client2", + "clock", + "collections", + "copilot", + "ctor", + "db2", + "env_logger 0.9.3", + "fs", + "fsevent", + "futures 0.3.28", + "fuzzy2", + "git", + "git2", + "globset", + "gpui2", + "ignore", + "itertools 0.10.5", + "language2", + "lazy_static", + "log", + "lsp2", + "node_runtime", + "parking_lot 0.11.2", + "postage", + "prettier", + "pretty_assertions", + "rand 0.8.5", + "regex", + "rpc", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "sha2 0.10.7", + "similar", + "smol", + "sum_tree", + "tempdir", + "terminal", + "text", + "thiserror", + "toml 0.5.11", + "unindent", + "util", +] + [[package]] name = "project_panel" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9090b830e2..3b301c8833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "crates/plugin_runtime", "crates/prettier", "crates/project", + "crates/project2", "crates/project_panel", "crates/project_symbols", "crates/recent_projects", diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 1e4852ff4d..1eb6ff37b1 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -17,13 +17,14 @@ use crate::{ }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; -use futures::Future; +use futures::{future::BoxFuture, Future}; use parking_lot::{Mutex, RwLock}; use slotmap::SlotMap; use std::{ any::{type_name, Any, TypeId}, mem, sync::{atomic::Ordering::SeqCst, Arc, Weak}, + time::Duration, }; use util::http::{self, HttpClient}; @@ -89,6 +90,7 @@ impl App { event_listeners: SubscriberSet::new(), release_listeners: SubscriberSet::new(), global_observers: SubscriberSet::new(), + quit_observers: SubscriberSet::new(), layout_id_buffer: Default::default(), propagate_event: true, }) @@ -155,11 +157,12 @@ impl App { } } +type ActionBuilder = fn(json: Option) -> anyhow::Result>; +type FrameCallback = Box; type Handler = Box bool + Send + Sync + 'static>; type Listener = Box bool + Send + Sync + 'static>; +type QuitHandler = Box BoxFuture<'static, ()> + Send + Sync + 'static>; type ReleaseListener = Box; -type FrameCallback = Box; -type ActionBuilder = fn(json: Option) -> anyhow::Result>; pub struct AppContext { this: Weak>, @@ -188,11 +191,33 @@ pub struct AppContext { pub(crate) event_listeners: SubscriberSet, pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, + pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. pub(crate) propagate_event: bool, } impl AppContext { + pub fn quit(&mut self) { + let mut futures = Vec::new(); + + self.quit_observers.clone().retain(&(), |observer| { + futures.push(observer(self)); + true + }); + + self.windows.clear(); + self.flush_effects(); + + let futures = futures::future::join_all(futures); + if self + .executor + .block_with_timeout(Duration::from_millis(100), futures) + .is_err() + { + log::error!("timed out waiting on app_will_quit"); + } + } + pub fn app_metadata(&self) -> AppMetadata { self.app_metadata.clone() } diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index a8c0bff709..176f5e4237 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -3,6 +3,7 @@ use crate::{ Subscription, Task, WeakHandle, }; use derive_more::{Deref, DerefMut}; +use futures::FutureExt; use std::{future::Future, marker::PhantomData}; #[derive(Deref, DerefMut)] @@ -84,6 +85,28 @@ impl<'a, T: Send + Sync + 'static> ModelContext<'a, T> { ) } + pub fn on_app_quit( + &mut self, + on_quit: impl Fn(&mut T, &mut AppContext) -> Fut + Send + Sync + 'static, + ) -> Subscription + where + Fut: 'static + Future + Send, + { + let handle = self.handle(); + self.app.quit_observers.insert( + (), + Box::new(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; + } + } + .boxed() + }), + ) + } + pub fn observe_release( &mut self, handle: &Handle, diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index d241b773f8..79582c11ad 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -152,14 +152,11 @@ impl Executor { } } - pub fn block_with_timeout( + pub fn block_with_timeout( &self, duration: Duration, - future: F, - ) -> Result> - where - F: Future + Send + Sync + 'static, - { + future: impl Future, + ) -> Result> { let mut future = Box::pin(future); let timeout = { let future = &mut future; diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index d2f13ba17b..a8b764abc9 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -832,7 +832,8 @@ impl Buffer { if parse_again { this.reparse(cx); } - }); + }) + .ok(); }) .detach(); } @@ -876,7 +877,8 @@ impl Buffer { let indent_sizes = indent_sizes.await; this.update(&mut cx, |this, cx| { this.apply_autoindents(indent_sizes, cx); - }); + }) + .ok(); })); } } diff --git a/crates/language2/src/syntax_map.rs b/crates/language2/src/syntax_map.rs index 18f2e9b264..4abb9afe7e 100644 --- a/crates/language2/src/syntax_map.rs +++ b/crates/language2/src/syntax_map.rs @@ -234,6 +234,7 @@ impl SyntaxMap { self.snapshot.interpolate(text); } + #[allow(dead_code)] // todo!() #[cfg(test)] pub fn reparse(&mut self, language: Arc, text: &BufferSnapshot) { self.snapshot @@ -785,6 +786,7 @@ impl SyntaxSnapshot { ) } + #[allow(dead_code)] // todo!() #[cfg(test)] pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec { self.layers_for_range(0..buffer.len(), buffer).collect() diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml new file mode 100644 index 0000000000..8ccadc62f7 --- /dev/null +++ b/crates/project2/Cargo.toml @@ -0,0 +1,84 @@ +[package] +name = "project2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project2.rs" +doctest = false + +[features] +test-support = [ + "client2/test-support", + "db2/test-support", + "language2/test-support", + "settings2/test-support", + "text/test-support", + "prettier/test-support", +] + +[dependencies] +text = { path = "../text" } +copilot = { path = "../copilot" } +client2 = { path = "../client2" } +clock = { path = "../clock" } +collections = { path = "../collections" } +db2 = { path = "../db2" } +fs = { path = "../fs" } +fsevent = { path = "../fsevent" } +fuzzy2 = { path = "../fuzzy2" } +git = { path = "../git" } +gpui2 = { path = "../gpui2" } +language2 = { path = "../language2" } +lsp2 = { path = "../lsp2" } +node_runtime = { path = "../node_runtime" } +prettier = { path = "../prettier" } +rpc = { path = "../rpc" } +settings2 = { path = "../settings2" } +sum_tree = { path = "../sum_tree" } +terminal = { path = "../terminal" } +util = { path = "../util" } + +aho-corasick = "1.1" +anyhow.workspace = true +async-trait.workspace = true +backtrace = "0.3" +futures.workspace = true +globset.workspace = true +ignore = "0.4" +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +regex.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +sha2 = "0.10" +similar = "1.3" +smol.workspace = true +thiserror.workspace = true +toml.workspace = true +itertools = "0.10" + +[dev-dependencies] +ctor.workspace = true +env_logger.workspace = true +pretty_assertions.workspace = true +client2 = { path = "../client2", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +db2 = { path = "../db2", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } +gpui2 = { path = "../gpui2", features = ["test-support"] } +language2 = { path = "../language2", features = ["test-support"] } +lsp2 = { path = "../lsp2", features = ["test-support"] } +settings2 = { path = "../settings2", features = ["test-support"] } +prettier = { path = "../prettier", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +git2.workspace = true +tempdir.workspace = true +unindent.workspace = true diff --git a/crates/project2/src/ignore.rs b/crates/project2/src/ignore.rs new file mode 100644 index 0000000000..8bac08b96c --- /dev/null +++ b/crates/project2/src/ignore.rs @@ -0,0 +1,57 @@ +use ignore::gitignore::Gitignore; +use std::{ffi::OsStr, path::Path, sync::Arc}; + +pub enum IgnoreStack { + None, + Some { + abs_base_path: Arc, + ignore: Arc, + parent: Arc, + }, + All, +} + +impl IgnoreStack { + pub fn none() -> Arc { + Arc::new(Self::None) + } + + pub fn all() -> Arc { + Arc::new(Self::All) + } + + pub fn is_all(&self) -> bool { + matches!(self, IgnoreStack::All) + } + + pub fn append(self: Arc, abs_base_path: Arc, ignore: Arc) -> Arc { + match self.as_ref() { + IgnoreStack::All => self, + _ => Arc::new(Self::Some { + abs_base_path, + ignore, + parent: self, + }), + } + } + + pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool { + if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) { + return true; + } + + match self { + Self::None => false, + Self::All => true, + Self::Some { + abs_base_path, + ignore, + parent: prev, + } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) { + ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir), + ignore::Match::Ignore(_) => true, + ignore::Match::Whitelist(_) => false, + }, + } + } +} diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs new file mode 100644 index 0000000000..383600e49b --- /dev/null +++ b/crates/project2/src/lsp_command.rs @@ -0,0 +1,2352 @@ +use crate::{ + DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + MarkupContent, Project, ProjectTransaction, ResolveState, +}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use client2::proto::{self, PeerId}; +use futures::future; +use gpui2::{AppContext, AsyncAppContext, Handle}; +use language2::{ + language_settings::{language_settings, InlayHintKind}, + point_from_lsp, point_to_lsp, + proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, +}; +use lsp2::{ + CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId, + OneOf, ServerCapabilities, +}; +use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; +use text::LineEnding; + +pub fn lsp_formatting_options(tab_size: u32) -> lsp2::FormattingOptions { + lsp2::FormattingOptions { + tab_size, + insert_spaces: true, + insert_final_newline: Some(true), + ..lsp2::FormattingOptions::default() + } +} + +#[async_trait(?Send)] +pub(crate) trait LspCommand: 'static + Sized { + type Response: 'static + Default + Send; + type LspRequest: 'static + Send + lsp2::request::Request; + type ProtoRequest: 'static + Send + proto::RequestMessage; + + fn check_capabilities(&self, _: &lsp2::ServerCapabilities) -> bool { + true + } + + fn to_lsp( + &self, + path: &Path, + buffer: &Buffer, + language_server: &Arc, + cx: &AppContext, + ) -> ::Params; + + async fn response_from_lsp( + self, + message: ::Result, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + cx: AsyncAppContext, + ) -> Result; + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; + + async fn from_proto( + message: Self::ProtoRequest, + project: Handle, + buffer: Handle, + cx: AsyncAppContext, + ) -> Result; + + fn response_to_proto( + response: Self::Response, + project: &mut Project, + peer_id: PeerId, + buffer_version: &clock::Global, + cx: &mut AppContext, + ) -> ::Response; + + async fn response_from_proto( + self, + message: ::Response, + project: Handle, + buffer: Handle, + cx: AsyncAppContext, + ) -> Result; + + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; +} + +pub(crate) struct PrepareRename { + pub position: PointUtf16, +} + +pub(crate) struct PerformRename { + pub position: PointUtf16, + pub new_name: String, + pub push_to_history: bool, +} + +pub(crate) struct GetDefinition { + pub position: PointUtf16, +} + +pub(crate) struct GetTypeDefinition { + pub position: PointUtf16, +} + +pub(crate) struct GetReferences { + pub position: PointUtf16, +} + +pub(crate) struct GetDocumentHighlights { + pub position: PointUtf16, +} + +pub(crate) struct GetHover { + pub position: PointUtf16, +} + +pub(crate) struct GetCompletions { + pub position: PointUtf16, +} + +pub(crate) struct GetCodeActions { + pub range: Range, +} + +pub(crate) struct OnTypeFormatting { + pub position: PointUtf16, + pub trigger: String, + pub options: FormattingOptions, + pub push_to_history: bool, +} + +pub(crate) struct InlayHints { + pub range: Range, +} + +pub(crate) struct FormattingOptions { + tab_size: u32, +} + +impl From for FormattingOptions { + fn from(value: lsp2::FormattingOptions) -> Self { + Self { + tab_size: value.tab_size, + } + } +} + +#[async_trait(?Send)] +impl LspCommand for PrepareRename { + type Response = Option>; + type LspRequest = lsp2::request::PrepareRenameRequest; + type ProtoRequest = proto::PrepareRename; + + fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { + if let Some(lsp2::OneOf::Right(rename)) = &capabilities.rename_provider { + rename.prepare_provider == Some(true) + } else { + false + } + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::TextDocumentPositionParams { + lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: Handle, + buffer: Handle, + _: LanguageServerId, + cx: AsyncAppContext, + ) -> Result>> { + buffer.read_with(&cx, |buffer, _| { + if let Some( + lsp2::PrepareRenameResponse::Range(range) + | lsp2::PrepareRenameResponse::RangeWithPlaceholder { range, .. }, + ) = message + { + let Range { start, end } = range_from_lsp(range); + if buffer.clip_point_utf16(start, Bias::Left) == start.0 + && buffer.clip_point_utf16(end, Bias::Left) == end.0 + { + return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end))); + } + } + Ok(None) + }) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename { + proto::PrepareRename { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::PrepareRename, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + range: Option>, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &mut AppContext, + ) -> proto::PrepareRenameResponse { + proto::PrepareRenameResponse { + can_rename: range.is_some(), + start: range + .as_ref() + .map(|range| language2::proto::serialize_anchor(&range.start)), + end: range + .as_ref() + .map(|range| language2::proto::serialize_anchor(&range.end)), + version: serialize_version(buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::PrepareRenameResponse, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result>> { + if message.can_rename { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + let start = message.start.and_then(deserialize_anchor); + let end = message.end.and_then(deserialize_anchor); + Ok(start.zip(end).map(|(start, end)| start..end)) + } else { + Ok(None) + } + } + + fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for PerformRename { + type Response = ProjectTransaction; + type LspRequest = lsp2::request::Rename; + type ProtoRequest = proto::PerformRename; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::RenameParams { + lsp2::RenameParams { + text_document_position: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + new_name: self.new_name.clone(), + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result { + if let Some(edit) = message { + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + Project::deserialize_workspace_edit( + project, + edit, + self.push_to_history, + lsp_adapter, + lsp_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) + } + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { + proto::PerformRename { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + new_name: self.new_name.clone(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::PerformRename, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + new_name: message.new_name, + push_to_history: false, + }) + } + + fn response_to_proto( + response: ProjectTransaction, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::PerformRenameResponse { + let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx); + proto::PerformRenameResponse { + transaction: Some(transaction), + } + } + + async fn response_from_proto( + self, + message: proto::PerformRenameResponse, + project: Handle, + _: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let message = message + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project + .update(&mut cx, |project, cx| { + project.deserialize_project_transaction(message, self.push_to_history, cx) + }) + .await + } + + fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetDefinition { + type Response = Vec; + type LspRequest = lsp2::request::GotoDefinition; + type ProtoRequest = proto::GetDefinition; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::GotoDefinitionParams { + lsp2::GotoDefinitionParams { + text_document_position_params: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_lsp(message, project, buffer, server_id, cx).await + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { + proto::GetDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetDefinition, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::GetDefinitionResponse { + let links = location_links_to_proto(response, project, peer_id, cx); + proto::GetDefinitionResponse { links } + } + + async fn response_from_proto( + self, + message: proto::GetDefinitionResponse, + project: Handle, + _: Handle, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_proto(message.links, project, cx).await + } + + fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetTypeDefinition { + type Response = Vec; + type LspRequest = lsp2::request::GotoTypeDefinition; + type ProtoRequest = proto::GetTypeDefinition; + + fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { + match &capabilities.type_definition_provider { + None => false, + Some(lsp2::TypeDefinitionProviderCapability::Simple(false)) => false, + _ => true, + } + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::GotoTypeDefinitionParams { + lsp2::GotoTypeDefinitionParams { + text_document_position_params: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_lsp(message, project, buffer, server_id, cx).await + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { + proto::GetTypeDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetTypeDefinition, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::GetTypeDefinitionResponse { + let links = location_links_to_proto(response, project, peer_id, cx); + proto::GetTypeDefinitionResponse { links } + } + + async fn response_from_proto( + self, + message: proto::GetTypeDefinitionResponse, + project: Handle, + _: Handle, + cx: AsyncAppContext, + ) -> Result> { + location_links_from_proto(message.links, project, cx).await + } + + fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> u64 { + message.buffer_id + } +} + +fn language_server_for_buffer( + project: &Handle, + buffer: &Handle, + server_id: LanguageServerId, + cx: &mut AsyncAppContext, +) -> Result<(Arc, Arc)> { + project + .read_with(cx, |project, cx| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) + .ok_or_else(|| anyhow!("no language server found for buffer")) +} + +async fn location_links_from_proto( + proto_links: Vec, + project: Handle, + mut cx: AsyncAppContext, +) -> Result> { + let mut links = Vec::new(); + + for link in proto_links { + let origin = match link.origin { + Some(origin) => { + let buffer = project + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(origin.buffer_id, cx) + }) + .await?; + let start = origin + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin start"))?; + let end = origin + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing origin end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) + .await?; + Some(Location { + buffer, + range: start..end, + }) + } + None => None, + }; + + let target = link.target.ok_or_else(|| anyhow!("missing target"))?; + let buffer = project + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(target.buffer_id, cx) + }) + .await?; + let start = target + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = target + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) + .await?; + let target = Location { + buffer, + range: start..end, + }; + + links.push(LocationLink { origin, target }) + } + + Ok(links) +} + +async fn location_links_from_lsp( + message: Option, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, +) -> Result> { + let message = match message { + Some(message) => message, + None => return Ok(Vec::new()), + }; + + let mut unresolved_links = Vec::new(); + match message { + lsp2::GotoDefinitionResponse::Scalar(loc) => { + unresolved_links.push((None, loc.uri, loc.range)); + } + + lsp2::GotoDefinitionResponse::Array(locs) => { + unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); + } + + lsp2::GotoDefinitionResponse::Link(links) => { + unresolved_links.extend(links.into_iter().map(|l| { + ( + l.origin_selection_range, + l.target_uri, + l.target_selection_range, + ) + })); + } + } + + let (lsp_adapter, language_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + let mut definitions = Vec::new(); + for (origin_range, target_uri, target_range) in unresolved_links { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let origin_location = origin_range.map(|origin_range| { + let origin_buffer = buffer.read(cx); + let origin_start = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); + let origin_end = + origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); + Location { + buffer: buffer.clone(), + range: origin_buffer.anchor_after(origin_start) + ..origin_buffer.anchor_before(origin_end), + } + }); + + let target_buffer = target_buffer_handle.read(cx); + let target_start = + target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = + target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + let target_location = Location { + buffer: target_buffer_handle, + range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }; + + definitions.push(LocationLink { + origin: origin_location, + target: target_location, + }) + }); + } + Ok(definitions) +} + +fn location_links_to_proto( + links: Vec, + project: &mut Project, + peer_id: PeerId, + cx: &mut AppContext, +) -> Vec { + links + .into_iter() + .map(|definition| { + let origin = definition.origin.map(|origin| { + let buffer_id = project.create_buffer_for_peer(&origin.buffer, peer_id, cx); + proto::Location { + start: Some(serialize_anchor(&origin.range.start)), + end: Some(serialize_anchor(&origin.range.end)), + buffer_id, + } + }); + + let buffer_id = project.create_buffer_for_peer(&definition.target.buffer, peer_id, cx); + let target = proto::Location { + start: Some(serialize_anchor(&definition.target.range.start)), + end: Some(serialize_anchor(&definition.target.range.end)), + buffer_id, + }; + + proto::LocationLink { + origin, + target: Some(target), + } + }) + .collect() +} + +#[async_trait(?Send)] +impl LspCommand for GetReferences { + type Response = Vec; + type LspRequest = lsp2::request::References; + type ProtoRequest = proto::GetReferences; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::ReferenceParams { + lsp2::ReferenceParams { + text_document_position: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + context: lsp2::ReferenceContext { + include_declaration: true, + }, + } + } + + async fn response_from_lsp( + self, + locations: Option>, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result> { + let mut references = Vec::new(); + let (lsp_adapter, language_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + + if let Some(locations) = locations { + for lsp_location in locations { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_via_lsp( + lsp_location.uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + references.push(Location { + buffer: target_buffer_handle, + range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }); + }); + } + } + + Ok(references) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetReferences { + proto::GetReferences { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetReferences, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut AppContext, + ) -> proto::GetReferencesResponse { + let locations = response + .into_iter() + .map(|definition| { + let buffer_id = project.create_buffer_for_peer(&definition.buffer, peer_id, cx); + proto::Location { + start: Some(serialize_anchor(&definition.range.start)), + end: Some(serialize_anchor(&definition.range.end)), + buffer_id, + } + }) + .collect(); + proto::GetReferencesResponse { locations } + } + + async fn response_from_proto( + self, + message: proto::GetReferencesResponse, + project: Handle, + _: Handle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut locations = Vec::new(); + for location in message.locations { + let target_buffer = project + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(location.buffer_id, cx) + }) + .await?; + let start = location + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = location + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + target_buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) + .await?; + locations.push(Location { + buffer: target_buffer, + range: start..end, + }) + } + Ok(locations) + } + + fn buffer_id_from_proto(message: &proto::GetReferences) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetDocumentHighlights { + type Response = Vec; + type LspRequest = lsp2::request::DocumentHighlightRequest; + type ProtoRequest = proto::GetDocumentHighlights; + + fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { + capabilities.document_highlight_provider.is_some() + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::DocumentHighlightParams { + lsp2::DocumentHighlightParams { + text_document_position_params: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + lsp_highlights: Option>, + _: Handle, + buffer: Handle, + _: LanguageServerId, + cx: AsyncAppContext, + ) -> Result> { + buffer.read_with(&cx, |buffer, _| { + let mut lsp_highlights = lsp_highlights.unwrap_or_default(); + lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end))); + Ok(lsp_highlights + .into_iter() + .map(|lsp_highlight| { + let start = buffer + .clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left); + let end = buffer + .clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left); + DocumentHighlight { + range: buffer.anchor_after(start)..buffer.anchor_before(end), + kind: lsp_highlight + .kind + .unwrap_or(lsp2::DocumentHighlightKind::READ), + } + }) + .collect()) + }) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights { + proto::GetDocumentHighlights { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetDocumentHighlights, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Vec, + _: &mut Project, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::GetDocumentHighlightsResponse { + let highlights = response + .into_iter() + .map(|highlight| proto::DocumentHighlight { + start: Some(serialize_anchor(&highlight.range.start)), + end: Some(serialize_anchor(&highlight.range.end)), + kind: match highlight.kind { + DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(), + DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(), + DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(), + _ => proto::document_highlight::Kind::Text.into(), + }, + }) + .collect(); + proto::GetDocumentHighlightsResponse { highlights } + } + + async fn response_from_proto( + self, + message: proto::GetDocumentHighlightsResponse, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut highlights = Vec::new(); + for highlight in message.highlights { + let start = highlight + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = highlight + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end])) + .await?; + let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { + Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, + Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ, + Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE, + None => DocumentHighlightKind::TEXT, + }; + highlights.push(DocumentHighlight { + range: start..end, + kind, + }); + } + Ok(highlights) + } + + fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetHover { + type Response = Option; + type LspRequest = lsp2::request::HoverRequest; + type ProtoRequest = proto::GetHover; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::HoverParams { + lsp2::HoverParams { + text_document_position_params: lsp2::TextDocumentPositionParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + }, + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: Handle, + buffer: Handle, + _: LanguageServerId, + cx: AsyncAppContext, + ) -> Result { + Ok(message.and_then(|hover| { + let (language, range) = cx.read(|cx| { + let buffer = buffer.read(cx); + ( + buffer.language().cloned(), + hover.range.map(|range| { + let token_start = + buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); + let token_end = + buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); + buffer.anchor_after(token_start)..buffer.anchor_before(token_end) + }), + ) + }); + + fn hover_blocks_from_marked_string( + marked_string: lsp2::MarkedString, + ) -> Option { + let block = match marked_string { + lsp2::MarkedString::String(content) => HoverBlock { + text: content, + kind: HoverBlockKind::Markdown, + }, + lsp2::MarkedString::LanguageString(lsp2::LanguageString { + language, + value, + }) => HoverBlock { + text: value, + kind: HoverBlockKind::Code { language }, + }, + }; + if block.text.is_empty() { + None + } else { + Some(block) + } + } + + let contents = cx.read(|_| match hover.contents { + lsp2::HoverContents::Scalar(marked_string) => { + hover_blocks_from_marked_string(marked_string) + .into_iter() + .collect() + } + lsp2::HoverContents::Array(marked_strings) => marked_strings + .into_iter() + .filter_map(hover_blocks_from_marked_string) + .collect(), + lsp2::HoverContents::Markup(markup_content) => vec![HoverBlock { + text: markup_content.value, + kind: if markup_content.kind == lsp2::MarkupKind::Markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + }], + }); + + Some(Hover { + contents, + range, + language, + }) + })) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { + proto::GetHover { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + version: serialize_version(&buffer.version), + } + } + + async fn from_proto( + message: Self::ProtoRequest, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + }) + } + + fn response_to_proto( + response: Self::Response, + _: &mut Project, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::GetHoverResponse { + if let Some(response) = response { + let (start, end) = if let Some(range) = response.range { + ( + Some(language2::proto::serialize_anchor(&range.start)), + Some(language2::proto::serialize_anchor(&range.end)), + ) + } else { + (None, None) + }; + + let contents = response + .contents + .into_iter() + .map(|block| proto::HoverBlock { + text: block.text, + is_markdown: block.kind == HoverBlockKind::Markdown, + language: if let HoverBlockKind::Code { language } = block.kind { + Some(language) + } else { + None + }, + }) + .collect(); + + proto::GetHoverResponse { + start, + end, + contents, + } + } else { + proto::GetHoverResponse { + start: None, + end: None, + contents: Vec::new(), + } + } + } + + async fn response_from_proto( + self, + message: proto::GetHoverResponse, + _: Handle, + buffer: Handle, + cx: AsyncAppContext, + ) -> Result { + let contents: Vec<_> = message + .contents + .into_iter() + .map(|block| HoverBlock { + text: block.text, + kind: if let Some(language) = block.language { + HoverBlockKind::Code { language } + } else if block.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + }) + .collect(); + if contents.is_empty() { + return Ok(None); + } + + let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned()); + let range = if let (Some(start), Some(end)) = (message.start, message.end) { + language2::proto::deserialize_anchor(start) + .and_then(|start| language2::proto::deserialize_anchor(end).map(|end| start..end)) + } else { + None + }; + + Ok(Some(Hover { + contents, + range, + language, + })) + } + + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetCompletions { + type Response = Vec; + type LspRequest = lsp2::request::Completion; + type ProtoRequest = proto::GetCompletions; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::CompletionParams { + lsp2::CompletionParams { + text_document_position: lsp2::TextDocumentPositionParams::new( + lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()), + point_to_lsp(self.position), + ), + context: Default::default(), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + completions: Option, + _: Handle, + buffer: Handle, + server_id: LanguageServerId, + cx: AsyncAppContext, + ) -> Result> { + let mut response_list = None; + let completions = if let Some(completions) = completions { + match completions { + lsp2::CompletionResponse::Array(completions) => completions, + + lsp2::CompletionResponse::List(mut list) => { + let items = std::mem::take(&mut list.items); + response_list = Some(list); + items + } + } + } else { + Default::default() + }; + + let completions = buffer.read_with(&cx, |buffer, _| { + let language = buffer.language().cloned(); + let snapshot = buffer.snapshot(); + let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); + + let mut range_for_token = None; + completions + .into_iter() + .filter_map(move |mut lsp_completion| { + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { + // If the language server provides a range to overwrite, then + // check that the range is valid. + Some(lsp2::CompletionTextEdit::Edit(edit)) => { + let range = range_from_lsp(edit.range); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + edit.new_text.clone(), + ) + } + + // If the language server does not provide a range, then infer + // the range based on the syntax tree. + None => { + if self.position != clipped_position { + log::info!("completion out of expected range"); + return None; + } + + let default_edit_range = response_list + .as_ref() + .and_then(|list| list.item_defaults.as_ref()) + .and_then(|defaults| defaults.edit_range.as_ref()) + .and_then(|range| match range { + CompletionListItemDefaultsEditRange::Range(r) => Some(r), + _ => None, + }); + + let range = if let Some(range) = default_edit_range { + let range = range_from_lsp(range.clone()); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + + snapshot.anchor_before(start)..snapshot.anchor_after(end) + } else { + range_for_token + .get_or_insert_with(|| { + let offset = self.position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + let range = if kind == Some(CharKind::Word) { + range + } else { + offset..offset + }; + + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .clone() + }; + + let text = lsp_completion + .insert_text + .as_ref() + .unwrap_or(&lsp_completion.label) + .clone(); + (range, text) + } + + Some(lsp2::CompletionTextEdit::InsertAndReplace(_)) => { + log::info!("unsupported insert/replace completion"); + return None; + } + }; + + let language = language.clone(); + LineEnding::normalize(&mut new_text); + Some(async move { + let mut label = None; + if let Some(language) = language { + language.process_completion(&mut lsp_completion).await; + label = language.label_for_completion(&lsp_completion).await; + } + Completion { + old_range, + new_text, + label: label.unwrap_or_else(|| { + language2::CodeLabel::plain( + lsp_completion.label.clone(), + lsp_completion.filter_text.as_deref(), + ) + }), + server_id, + lsp_completion, + } + }) + }) + }); + + Ok(future::join_all(completions).await) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { + let anchor = buffer.anchor_after(self.position); + proto::GetCompletions { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor(&anchor)), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetCompletions, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let version = deserialize_version(&message.version); + buffer + .update(&mut cx, |buffer, _| buffer.wait_for_version(version)) + .await?; + let position = message + .position + .and_then(language2::proto::deserialize_anchor) + .map(|p| { + buffer.read_with(&cx, |buffer, _| { + buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) + }) + }) + .ok_or_else(|| anyhow!("invalid position"))?; + Ok(Self { position }) + } + + fn response_to_proto( + completions: Vec, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &mut AppContext, + ) -> proto::GetCompletionsResponse { + proto::GetCompletionsResponse { + completions: completions + .iter() + .map(language2::proto::serialize_completion) + .collect(), + version: serialize_version(&buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::GetCompletionsResponse, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned()); + let completions = message.completions.into_iter().map(|completion| { + language2::proto::deserialize_completion(completion, language.clone()) + }); + future::try_join_all(completions).await + } + + fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetCodeActions { + type Response = Vec; + type LspRequest = lsp2::request::CodeActionRequest; + type ProtoRequest = proto::GetCodeActions; + + fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { + match &capabilities.code_action_provider { + None => false, + Some(lsp2::CodeActionProviderCapability::Simple(false)) => false, + _ => true, + } + } + + fn to_lsp( + &self, + path: &Path, + buffer: &Buffer, + language_server: &Arc, + _: &AppContext, + ) -> lsp2::CodeActionParams { + let relevant_diagnostics = buffer + .snapshot() + .diagnostics_in_range::<_, usize>(self.range.clone(), false) + .map(|entry| entry.to_lsp_diagnostic_stub()) + .collect(); + lsp2::CodeActionParams { + text_document: lsp2::TextDocumentIdentifier::new( + lsp2::Url::from_file_path(path).unwrap(), + ), + range: range_to_lsp(self.range.to_point_utf16(buffer)), + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + context: lsp2::CodeActionContext { + diagnostics: relevant_diagnostics, + only: language_server.code_action_kinds(), + ..lsp2::CodeActionContext::default() + }, + } + } + + async fn response_from_lsp( + self, + actions: Option, + _: Handle, + _: Handle, + server_id: LanguageServerId, + _: AsyncAppContext, + ) -> Result> { + Ok(actions + .unwrap_or_default() + .into_iter() + .filter_map(|entry| { + if let lsp2::CodeActionOrCommand::CodeAction(lsp_action) = entry { + Some(CodeAction { + server_id, + range: self.range.clone(), + lsp_action, + }) + } else { + None + } + }) + .collect()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions { + proto::GetCodeActions { + project_id, + buffer_id: buffer.remote_id(), + start: Some(language2::proto::serialize_anchor(&self.range.start)), + end: Some(language2::proto::serialize_anchor(&self.range.end)), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetCodeActions, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let start = message + .start + .and_then(language2::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid start"))?; + let end = message + .end + .and_then(language2::proto::deserialize_anchor) + .ok_or_else(|| anyhow!("invalid end"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + Ok(Self { range: start..end }) + } + + fn response_to_proto( + code_actions: Vec, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &mut AppContext, + ) -> proto::GetCodeActionsResponse { + proto::GetCodeActionsResponse { + actions: code_actions + .iter() + .map(language2::proto::serialize_code_action) + .collect(), + version: serialize_version(&buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::GetCodeActionsResponse, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + message + .actions + .into_iter() + .map(language2::proto::deserialize_code_action) + .collect() + } + + fn buffer_id_from_proto(message: &proto::GetCodeActions) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for OnTypeFormatting { + type Response = Option; + type LspRequest = lsp2::request::OnTypeFormatting; + type ProtoRequest = proto::OnTypeFormatting; + + fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool { + let Some(on_type_formatting_options) = + &server_capabilities.document_on_type_formatting_provider + else { + return false; + }; + on_type_formatting_options + .first_trigger_character + .contains(&self.trigger) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(&self.trigger)) + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::DocumentOnTypeFormattingParams { + lsp2::DocumentOnTypeFormattingParams { + text_document_position: lsp2::TextDocumentPositionParams::new( + lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path(path).unwrap()), + point_to_lsp(self.position), + ), + ch: self.trigger.clone(), + options: lsp_formatting_options(self.options.tab_size), + } + } + + async fn response_from_lsp( + self, + message: Option>, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result> { + if let Some(edits) = message { + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + Project::deserialize_edits( + project, + buffer, + edits, + self.push_to_history, + lsp_adapter, + lsp_server, + &mut cx, + ) + .await + } else { + Ok(None) + } + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting { + proto::OnTypeFormatting { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language2::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + trigger: self.trigger.clone(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::OnTypeFormatting, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + let tab_size = buffer.read_with(&cx, |buffer, cx| { + language_settings(buffer.language(), buffer.file(), cx).tab_size + }); + + Ok(Self { + position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)), + trigger: message.trigger.clone(), + options: lsp_formatting_options(tab_size.get()).into(), + push_to_history: false, + }) + } + + fn response_to_proto( + response: Option, + _: &mut Project, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::OnTypeFormattingResponse { + proto::OnTypeFormattingResponse { + transaction: response + .map(|transaction| language2::proto::serialize_transaction(&transaction)), + } + } + + async fn response_from_proto( + self, + message: proto::OnTypeFormattingResponse, + _: Handle, + _: Handle, + _: AsyncAppContext, + ) -> Result> { + let Some(transaction) = message.transaction else { + return Ok(None); + }; + Ok(Some(language2::proto::deserialize_transaction( + transaction, + )?)) + } + + fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> u64 { + message.buffer_id + } +} + +impl InlayHints { + pub async fn lsp2_to_project_hint( + lsp_hint: lsp2::InlayHint, + buffer_handle: &Handle, + server_id: LanguageServerId, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp2::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp2::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) + .await + .context("lsp to project inlay hint conversion")?; + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + + Ok(InlayHint { + position, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp2::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp2::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + }) + } + + async fn lsp2_inlay_label_to_project( + lsp_label: lsp2::InlayHintLabel, + server_id: LanguageServerId, + ) -> anyhow::Result { + let label = match lsp_label { + lsp2::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp2::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts = Vec::with_capacity(lsp_parts.len()); + for lsp_part in lsp_parts { + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp2::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp2::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp2::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp2::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: Some(server_id).zip(lsp_part.location), + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) + } + + pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), + ResolveState::CanResolve(server_id, resolve_data) => ( + 1, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + position: Some(language2::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| { + let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); + let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location_url, + location_range_start, + location_range_end, + language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), + }}).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language2::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: { + match part + .location_url + .zip( + part.location_range_start.and_then(|start| { + Some(start..part.location_range_end?) + }), + ) + .zip(part.language_server_id) + { + Some(((uri, range), server_id)) => Some(( + LanguageServerId(server_id as usize), + lsp2::Location { + uri: lsp2::Url::parse(&uri) + .context("invalid uri in hint part {part:?}")?, + range: lsp2::Range::new( + point_to_lsp(PointUtf16::new( + range.start.row, + range.start.column, + )), + point_to_lsp(PointUtf16::new( + range.end.row, + range.end.column, + )), + ), + }, + )), + None => None, + } + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + pub fn project_to_lsp2_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp2::InlayHint { + lsp2::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp2::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp2::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp2::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp2::InlayHintTooltip::MarkupContent(lsp2::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp2::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp2::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp2::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp2::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp2::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp2::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp2::InlayHintLabelPartTooltip::MarkupContent( + lsp2::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp2::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp2::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.map(|(_, location)| location), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp2::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp2::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } +} + +#[async_trait(?Send)] +impl LspCommand for InlayHints { + type Response = Vec; + type LspRequest = lsp2::InlayHintRequest; + type ProtoRequest = proto::InlayHints; + + fn check_capabilities(&self, server_capabilities: &lsp2::ServerCapabilities) -> bool { + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { + return false; + }; + match inlay_hint_provider { + lsp2::OneOf::Left(enabled) => *enabled, + lsp2::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { + lsp2::InlayHintServerCapabilities::Options(_) => true, + lsp2::InlayHintServerCapabilities::RegistrationOptions(_) => false, + }, + } + } + + fn to_lsp( + &self, + path: &Path, + buffer: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp2::InlayHintParams { + lsp2::InlayHintParams { + text_document: lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(path).unwrap(), + }, + range: range_to_lsp(self.range.to_point_utf16(buffer)), + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option>, + project: Handle, + buffer: Handle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> anyhow::Result> { + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + // `typescript-language-server` adds padding to the left for type hints, turning + // `const foo: boolean` into `const foo : boolean` which looks odd. + // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. + // + // We could trim the whole string, but being pessimistic on par with the situation above, + // there might be a hint with multiple whitespaces at the end(s) which we need to display properly. + // Hence let's use a heuristic first to handle the most awkward case and look for more. + let force_no_type_left_padding = + lsp_adapter.name.0.as_ref() == "typescript-language-server"; + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved + }; + + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { + proto::InlayHints { + project_id, + buffer_id: buffer.remote_id(), + start: Some(language2::proto::serialize_anchor(&self.range.start)), + end: Some(language2::proto::serialize_anchor(&self.range.end)), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::InlayHints, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> Result { + let start = message + .start + .and_then(language2::proto::deserialize_anchor) + .context("invalid start")?; + let end = message + .end + .and_then(language2::proto::deserialize_anchor) + .context("invalid end")?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + Ok(Self { range: start..end }) + } + + fn response_to_proto( + response: Vec, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &mut AppContext, + ) -> proto::InlayHintsResponse { + proto::InlayHintsResponse { + hints: response + .into_iter() + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint)) + .collect(), + version: serialize_version(buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::InlayHintsResponse, + _: Handle, + buffer: Handle, + mut cx: AsyncAppContext, + ) -> anyhow::Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + let mut hints = Vec::new(); + for message_hint in message.hints { + hints.push(InlayHints::proto_to_project_hint(message_hint)?); + } + + Ok(hints) + } + + fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 { + message.buffer_id + } +} diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs new file mode 100644 index 0000000000..81d19c7861 --- /dev/null +++ b/crates/project2/src/project2.rs @@ -0,0 +1,8846 @@ +mod ignore; +mod lsp_command; +pub mod project_settings; +pub mod search; +pub mod terminals; +pub mod worktree; + +#[cfg(test)] +mod project_tests; +#[cfg(test)] +mod worktree_tests; + +use anyhow::{anyhow, Context, Result}; +use client2::{proto, Client, Collaborator, TypedEnvelope, UserStore}; +use clock::ReplicaId; +use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use copilot::Copilot; +use futures::{ + channel::{ + mpsc::{self, UnboundedReceiver}, + oneshot, + }, + future::{self, try_join_all, Shared}, + stream::FuturesUnordered, + AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, +}; +use globset::{Glob, GlobSet, GlobSetBuilder}; +use gpui2::{ + AnyHandle, AppContext, AsyncAppContext, EventEmitter, Handle, ModelContext, Task, WeakHandle, +}; +use itertools::Itertools; +use language2::{ + language_settings::{ + language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, + }, + point_to_lsp, + proto::{ + deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, + serialize_anchor, serialize_version, split_operations, + }, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter, + CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, + Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, + LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, + TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, +}; +use log::error; +use lsp2::{ + DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, + DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, +}; +use lsp_command::*; +use node_runtime::NodeRuntime; +use postage::watch; +use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; +use project_settings::{LspSettings, ProjectSettings}; +use rand::prelude::*; +use search::SearchQuery; +use serde::Serialize; +use settings2::SettingsStore; +use sha2::{Digest, Sha256}; +use similar::{ChangeTag, TextDiff}; +use smol::channel::{Receiver, Sender}; +use std::{ + cmp::{self, Ordering}, + convert::TryInto, + hash::Hash, + mem, + num::NonZeroU32, + ops::Range, + path::{self, Component, Path, PathBuf}, + process::Stdio, + str, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, + time::{Duration, Instant}, +}; +use terminals::Terminals; +use text::{Anchor, LineEnding, Rope}; +use util::{ + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, +}; + +pub use fs::*; +pub use worktree::*; + +pub trait Item { + fn entry_id(&self, cx: &AppContext) -> Option; + fn project_path(&self, cx: &AppContext) -> Option; +} + +// Language server state is stored across 3 collections: +// language_servers => +// a mapping from unique server id to LanguageServerState which can either be a task for a +// server in the process of starting, or a running server with adapter and language server arcs +// language_server_ids => a mapping from worktreeId and server name to the unique server id +// language_server_statuses => a mapping from unique server id to the current server status +// +// Multiple worktrees can map to the same language server for example when you jump to the definition +// of a file in the standard library. So language_server_ids is used to look up which server is active +// for a given worktree and language server name +// +// When starting a language server, first the id map is checked to make sure a server isn't already available +// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and +// the Starting variant of LanguageServerState is stored in the language_servers map. +pub struct Project { + worktrees: Vec, + active_entry: Option, + buffer_ordered_messages_tx: mpsc::UnboundedSender, + languages: Arc, + supplementary_language_servers: + HashMap)>, + language_servers: HashMap, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + language_server_statuses: BTreeMap, + last_workspace_edits_by_language_server: HashMap, + client: Arc, + next_entry_id: Arc, + join_project_response_message_id: u32, + next_diagnostic_group_id: usize, + user_store: Handle, + fs: Arc, + client_state: Option, + collaborators: HashMap, + client_subscriptions: Vec, + _subscriptions: Vec, + next_buffer_id: u64, + opened_buffer: (watch::Sender<()>, watch::Receiver<()>), + shared_buffers: HashMap>, + #[allow(clippy::type_complexity)] + loading_buffers_by_path: HashMap< + ProjectPath, + postage::watch::Receiver, Arc>>>, + >, + #[allow(clippy::type_complexity)] + loading_local_worktrees: + HashMap, Shared, Arc>>>>, + opened_buffers: HashMap, + local_buffer_ids_by_path: HashMap, + local_buffer_ids_by_entry_id: HashMap, + /// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it. + /// Used for re-issuing buffer requests when peers temporarily disconnect + incomplete_remote_buffers: HashMap>>, + buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots + buffers_being_formatted: HashSet, + buffers_needing_diff: HashSet>, + git_diff_debouncer: DelayedDebounced, + nonce: u128, + _maintain_buffer_languages: Task<()>, + _maintain_workspace_config: Task<()>, + terminals: Terminals, + copilot_lsp_subscription: Option, + copilot_log_subscription: Option, + current_lsp_settings: HashMap, LspSettings>, + node: Option>, + prettier_instances: HashMap< + (Option, PathBuf), + Shared, Arc>>>, + >, +} + +struct DelayedDebounced { + task: Option>, + cancel_channel: Option>, +} + +enum LanguageServerToQuery { + Primary, + Other(LanguageServerId), +} + +impl DelayedDebounced { + fn new() -> DelayedDebounced { + DelayedDebounced { + task: None, + cancel_channel: None, + } + } + + fn fire_new(&mut self, delay: Duration, cx: &mut ModelContext, func: F) + where + F: 'static + FnOnce(&mut Project, &mut ModelContext) -> Task<()>, + { + if let Some(channel) = self.cancel_channel.take() { + _ = channel.send(()); + } + + let (sender, mut receiver) = oneshot::channel::<()>(); + self.cancel_channel = Some(sender); + + let previous_task = self.task.take(); + self.task = Some(cx.spawn(|workspace, mut cx| async move { + let mut timer = cx.background().timer(delay).fuse(); + if let Some(previous_task) = previous_task { + previous_task.await; + } + + futures::select_biased! { + _ = receiver => return, + _ = timer => {} + } + + workspace + .update(&mut cx, |workspace, cx| (func)(workspace, cx)) + .await; + })); + } +} + +struct LspBufferSnapshot { + version: i32, + snapshot: TextBufferSnapshot, +} + +/// Message ordered with respect to buffer operations +enum BufferOrderedMessage { + Operation { + buffer_id: u64, + operation: proto::Operation, + }, + LanguageServerUpdate { + language_server_id: LanguageServerId, + message: proto::update_language_server::Variant, + }, + Resync, +} + +enum LocalProjectUpdate { + WorktreesChanged, + CreateBufferForPeer { + peer_id: proto::PeerId, + buffer_id: u64, + }, +} + +enum OpenBuffer { + Strong(Handle), + Weak(WeakHandle), + Operations(Vec), +} + +#[derive(Clone)] +enum WorktreeHandle { + Strong(Handle), + Weak(WeakHandle), +} + +enum ProjectClientState { + Local { + remote_id: u64, + updates_tx: mpsc::UnboundedSender, + _send_updates: Task<()>, + }, + Remote { + sharing_has_stopped: bool, + remote_id: u64, + replica_id: ReplicaId, + }, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Event { + LanguageServerAdded(LanguageServerId), + LanguageServerRemoved(LanguageServerId), + LanguageServerLog(LanguageServerId, String), + Notification(String), + ActiveEntryChanged(Option), + ActivateProjectPanel, + WorktreeAdded, + WorktreeRemoved(WorktreeId), + WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), + DiskBasedDiagnosticsStarted { + language_server_id: LanguageServerId, + }, + DiskBasedDiagnosticsFinished { + language_server_id: LanguageServerId, + }, + DiagnosticsUpdated { + path: ProjectPath, + language_server_id: LanguageServerId, + }, + RemoteIdChanged(Option), + DisconnectedFromHost, + Closed, + DeletedEntry(ProjectEntryId), + CollaboratorUpdated { + old_peer_id: proto::PeerId, + new_peer_id: proto::PeerId, + }, + CollaboratorJoined(proto::PeerId), + CollaboratorLeft(proto::PeerId), + RefreshInlayHints, +} + +pub enum LanguageServerState { + Starting(Task>>), + + Running { + language: Arc, + adapter: Arc, + server: Arc, + watched_paths: HashMap, + simulate_disk_based_diagnostics_completion: Option>, + }, +} + +#[derive(Serialize)] +pub struct LanguageServerStatus { + pub name: String, + pub pending_work: BTreeMap, + pub has_pending_diagnostic_updates: bool, + progress_tokens: HashSet, +} + +#[derive(Clone, Debug, Serialize)] +pub struct LanguageServerProgress { + pub message: Option, + pub percentage: Option, + #[serde(skip_serializing)] + pub last_update_at: Instant, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct ProjectPath { + pub worktree_id: WorktreeId, + pub path: Arc, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] +pub struct DiagnosticSummary { + pub error_count: usize, + pub warning_count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Location { + pub buffer: Handle, + pub range: Range, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlayHint { + pub position: language2::Anchor, + pub label: InlayHintLabel, + pub kind: Option, + pub padding_left: bool, + pub padding_right: bool, + pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(LanguageServerId, Option), + Resolving, +} + +impl InlayHint { + pub fn text(&self) -> String { + match &self.label { + InlayHintLabel::String(s) => s.to_owned(), + InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InlayHintLabel { + String(String), + LabelParts(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlayHintLabelPart { + pub value: String, + pub tooltip: Option, + pub location: Option<(LanguageServerId, lsp2::Location)>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InlayHintTooltip { + String(String), + MarkupContent(MarkupContent), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InlayHintLabelPartTooltip { + String(String), + MarkupContent(MarkupContent), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MarkupContent { + pub kind: HoverBlockKind, + pub value: String, +} + +#[derive(Debug, Clone)] +pub struct LocationLink { + pub origin: Option, + pub target: Location, +} + +#[derive(Debug)] +pub struct DocumentHighlight { + pub range: Range, + pub kind: DocumentHighlightKind, +} + +#[derive(Clone, Debug)] +pub struct Symbol { + pub language_server_name: LanguageServerName, + pub source_worktree_id: WorktreeId, + pub path: ProjectPath, + pub label: CodeLabel, + pub name: String, + pub kind: lsp2::SymbolKind, + pub range: Range>, + pub signature: [u8; 32], +} + +#[derive(Clone, Debug, PartialEq)] +pub struct HoverBlock { + pub text: String, + pub kind: HoverBlockKind, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HoverBlockKind { + PlainText, + Markdown, + Code { language: String }, +} + +#[derive(Debug)] +pub struct Hover { + pub contents: Vec, + pub range: Option>, + pub language: Option>, +} + +impl Hover { + pub fn is_empty(&self) -> bool { + self.contents.iter().all(|block| block.text.is_empty()) + } +} + +#[derive(Default)] +pub struct ProjectTransaction(pub HashMap, language2::Transaction>); + +impl DiagnosticSummary { + fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { + let mut this = Self { + error_count: 0, + warning_count: 0, + }; + + for entry in diagnostics { + if entry.diagnostic.is_primary { + match entry.diagnostic.severity { + DiagnosticSeverity::ERROR => this.error_count += 1, + DiagnosticSeverity::WARNING => this.warning_count += 1, + _ => {} + } + } + } + + this + } + + pub fn is_empty(&self) -> bool { + self.error_count == 0 && self.warning_count == 0 + } + + pub fn to_proto( + &self, + language_server_id: LanguageServerId, + path: &Path, + ) -> proto::DiagnosticSummary { + proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: language_server_id.0 as u64, + error_count: self.error_count as u32, + warning_count: self.warning_count as u32, + } + } +} + +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProjectEntryId(usize); + +impl ProjectEntryId { + pub const MAX: Self = Self(usize::MAX); + + pub fn new(counter: &AtomicUsize) -> Self { + Self(counter.fetch_add(1, SeqCst)) + } + + pub fn from_proto(id: u64) -> Self { + Self(id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + + pub fn to_usize(&self) -> usize { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatTrigger { + Save, + Manual, +} + +struct ProjectLspAdapterDelegate { + project: Handle, + http_client: Arc, +} + +impl FormatTrigger { + fn from_proto(value: i32) -> FormatTrigger { + match value { + 0 => FormatTrigger::Save, + 1 => FormatTrigger::Manual, + _ => FormatTrigger::Save, + } + } +} +#[derive(Clone, Debug, PartialEq)] +enum SearchMatchCandidate { + OpenBuffer { + buffer: Handle, + // This might be an unnamed file without representation on filesystem + path: Option>, + }, + Path { + worktree_id: WorktreeId, + path: Arc, + }, +} + +type SearchMatchCandidateIndex = usize; +impl SearchMatchCandidate { + fn path(&self) -> Option> { + match self { + SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(), + SearchMatchCandidate::Path { path, .. } => Some(path.clone()), + } + } +} + +impl Project { + pub fn init_settings(cx: &mut AppContext) { + settings2::register::(cx); + } + + pub fn init(client: &Arc, cx: &mut AppContext) { + Self::init_settings(cx); + + client.add_model_message_handler(Self::handle_add_collaborator); + client.add_model_message_handler(Self::handle_update_project_collaborator); + client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_buffer_reloaded); + client.add_model_message_handler(Self::handle_buffer_saved); + client.add_model_message_handler(Self::handle_start_language_server); + client.add_model_message_handler(Self::handle_update_language_server); + client.add_model_message_handler(Self::handle_update_project); + client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_create_buffer_for_peer); + client.add_model_message_handler(Self::handle_update_buffer_file); + client.add_model_request_handler(Self::handle_update_buffer); + client.add_model_message_handler(Self::handle_update_diagnostic_summary); + client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_update_worktree_settings); + client.add_model_request_handler(Self::handle_create_project_entry); + client.add_model_request_handler(Self::handle_rename_project_entry); + client.add_model_request_handler(Self::handle_copy_project_entry); + client.add_model_request_handler(Self::handle_delete_project_entry); + client.add_model_request_handler(Self::handle_expand_project_entry); + client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); + client.add_model_request_handler(Self::handle_apply_code_action); + client.add_model_request_handler(Self::handle_on_type_formatting); + client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); + client.add_model_request_handler(Self::handle_refresh_inlay_hints); + client.add_model_request_handler(Self::handle_reload_buffers); + client.add_model_request_handler(Self::handle_synchronize_buffers); + client.add_model_request_handler(Self::handle_format_buffers); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_search_project); + client.add_model_request_handler(Self::handle_get_project_symbols); + client.add_model_request_handler(Self::handle_open_buffer_for_symbol); + client.add_model_request_handler(Self::handle_open_buffer_by_id); + client.add_model_request_handler(Self::handle_open_buffer_by_path); + client.add_model_request_handler(Self::handle_save_buffer); + client.add_model_message_handler(Self::handle_update_diff_base); + } + + pub fn local( + client: Arc, + node: Arc, + user_store: Handle, + languages: Arc, + fs: Arc, + cx: &mut AppContext, + ) -> Handle { + cx.add_model(|cx: &mut ModelContext| { + let (tx, rx) = mpsc::unbounded(); + cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) + .detach(); + let copilot_lsp_subscription = + Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx)); + Self { + worktrees: Default::default(), + buffer_ordered_messages_tx: tx, + collaborators: Default::default(), + next_buffer_id: 0, + opened_buffers: Default::default(), + shared_buffers: Default::default(), + incomplete_remote_buffers: Default::default(), + loading_buffers_by_path: Default::default(), + loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), + buffer_snapshots: Default::default(), + join_project_response_message_id: 0, + client_state: None, + opened_buffer: watch::channel(), + client_subscriptions: Vec::new(), + _subscriptions: vec![ + cx.observe_global::(Self::on_settings_changed), + cx.on_release(Self::release), + ], + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), + active_entry: None, + languages, + client, + user_store, + fs, + next_entry_id: Default::default(), + next_diagnostic_group_id: Default::default(), + supplementary_language_servers: HashMap::default(), + language_servers: Default::default(), + language_server_ids: Default::default(), + language_server_statuses: Default::default(), + last_workspace_edits_by_language_server: Default::default(), + buffers_being_formatted: Default::default(), + buffers_needing_diff: Default::default(), + git_diff_debouncer: DelayedDebounced::new(), + nonce: StdRng::from_entropy().gen(), + terminals: Terminals { + local_handles: Vec::new(), + }, + copilot_lsp_subscription, + copilot_log_subscription: None, + current_lsp_settings: settings2::get::(cx).lsp2.clone(), + node: Some(node), + prettier_instances: HashMap::default(), + } + }) + } + + pub async fn remote( + remote_id: u64, + client: Arc, + user_store: Handle, + languages: Arc, + fs: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + client.authenticate_and_connect(true, &cx).await?; + + let subscription = client.subscribe_to_entity(remote_id)?; + let response = client + .request_envelope(proto::JoinProject { + project_id: remote_id, + }) + .await?; + let this = cx.add_model(|cx| { + let replica_id = response.payload.replica_id as ReplicaId; + + let mut worktrees = Vec::new(); + for worktree in response.payload.worktrees { + let worktree = cx.update(|cx| { + Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx) + }); + worktrees.push(worktree); + } + + let (tx, rx) = mpsc::unbounded(); + cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx)) + .detach(); + let copilot_lsp_subscription = + Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx)); + let mut this = Self { + worktrees: Vec::new(), + buffer_ordered_messages_tx: tx, + loading_buffers_by_path: Default::default(), + next_buffer_id: 0, + opened_buffer: watch::channel(), + shared_buffers: Default::default(), + incomplete_remote_buffers: Default::default(), + loading_local_worktrees: Default::default(), + local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), + active_entry: None, + collaborators: Default::default(), + join_project_response_message_id: response.message_id, + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), + languages, + user_store: user_store.clone(), + fs, + next_entry_id: Default::default(), + next_diagnostic_group_id: Default::default(), + client_subscriptions: Default::default(), + _subscriptions: vec![cx.on_release(Self::release)], + client: client.clone(), + client_state: Some(ProjectClientState::Remote { + sharing_has_stopped: false, + remote_id, + replica_id, + }), + supplementary_language_servers: HashMap::default(), + language_servers: Default::default(), + language_server_ids: Default::default(), + language_server_statuses: response + .payload + .language_servers + .into_iter() + .map(|server| { + ( + LanguageServerId(server.id as usize), + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ) + }) + .collect(), + last_workspace_edits_by_language_server: Default::default(), + opened_buffers: Default::default(), + buffers_being_formatted: Default::default(), + buffers_needing_diff: Default::default(), + git_diff_debouncer: DelayedDebounced::new(), + buffer_snapshots: Default::default(), + nonce: StdRng::from_entropy().gen(), + terminals: Terminals { + local_handles: Vec::new(), + }, + copilot_lsp_subscription, + copilot_log_subscription: None, + current_lsp_settings: settings2::get::(cx).lsp2.clone(), + node: None, + prettier_instances: HashMap::default(), + }; + for worktree in worktrees { + let _ = this.add_worktree(&worktree, cx); + } + this + }); + let subscription = subscription.set_model(&this, &mut cx); + + let user_ids = response + .payload + .collaborators + .iter() + .map(|peer| peer.user_id) + .collect(); + user_store + .update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx)) + .await?; + + this.update(&mut cx, |this, cx| { + this.set_collaborators_from_proto(response.payload.collaborators, cx)?; + this.client_subscriptions.push(subscription); + anyhow::Ok(()) + })?; + + Ok(this) + } + + fn release(&mut self, cx: &mut AppContext) { + match &self.client_state { + Some(ProjectClientState::Local { .. }) => { + let _ = self.unshare_internal(cx); + } + Some(ProjectClientState::Remote { remote_id, .. }) => { + let _ = self.client.send(proto::LeaveProject { + project_id: *remote_id, + }); + self.disconnected_from_host_internal(cx); + } + _ => {} + } + } + + // #[cfg(any(test, feature = "test-support"))] + // pub async fn test( + // fs: Arc, + // root_paths: impl IntoIterator, + // cx: &mut gpui::TestAppContext, + // ) -> Handle { + // let mut languages = LanguageRegistry::test(); + // languages.set_executor(cx.background()); + // let http_client = util::http::FakeHttpClient::with_404_response(); + // let client = cx.update(|cx| client2::Client::new(http_client.clone(), cx)); + // let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + // let project = cx.update(|cx| { + // Project::local( + // client, + // node_runtime::FakeNodeRuntime::new(), + // user_store, + // Arc::new(languages), + // fs, + // cx, + // ) + // }); + // for path in root_paths { + // let (tree, _) = project + // .update(cx, |project, cx| { + // project.find_or_create_local_worktree(path, true, cx) + // }) + // .await + // .unwrap(); + // tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + // .await; + // } + // project + // } + + /// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes. + /// Instead, if appends the suffix to every input, this suffix is returned by this method. + #[cfg(any(test, feature = "test-support"))] + pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str { + self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support( + plugins, + )); + Prettier::FORMAT_SUFFIX + } + + fn on_settings_changed(&mut self, cx: &mut ModelContext) { + let mut language_servers_to_start = Vec::new(); + let mut language_formatters_to_check = Vec::new(); + for buffer in self.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade() { + let buffer = buffer.read(cx); + let buffer_file = File::from_dyn(buffer.file()); + let buffer_language = buffer.language(); + let settings = language_settings(buffer_language, buffer.file(), cx); + if let Some(language) = buffer_language { + if settings.enable_language_server { + if let Some(file) = buffer_file { + language_servers_to_start + .push((file.worktree.clone(), Arc::clone(language))); + } + } + language_formatters_to_check.push(( + buffer_file.map(|f| f.worktree_id(cx)), + Arc::clone(language), + settings.clone(), + )); + } + } + } + + let mut language_servers_to_stop = Vec::new(); + let mut language_servers_to_restart = Vec::new(); + let languages = self.languages.to_vec(); + + let new_lsp_settings = settings2::get::(cx).lsp2.clone(); + let current_lsp_settings = &self.current_lsp_settings; + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { + let language = languages.iter().find_map(|l| { + let adapter = l + .lsp_adapters() + .iter() + .find(|adapter| &adapter.name == started_lsp_name)?; + Some((l, adapter)) + }); + if let Some((language, adapter)) = language { + let worktree = self.worktree_for_id(*worktree_id, cx); + let file = worktree.as_ref().and_then(|tree| { + tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) + }); + if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { + language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); + } else if let Some(worktree) = worktree { + let server_name = &adapter.name.0; + match ( + current_lsp_settings.get(server_name), + new_lsp_settings.get(server_name), + ) { + (None, None) => {} + (Some(_), None) | (None, Some(_)) => { + language_servers_to_restart.push((worktree, Arc::clone(language))); + } + (Some(current_lsp_settings), Some(new_lsp_settings)) => { + if current_lsp_settings != new_lsp_settings { + language_servers_to_restart.push((worktree, Arc::clone(language))); + } + } + } + } + } + } + self.current_lsp_settings = new_lsp_settings; + + // Stop all newly-disabled language servers. + for (worktree_id, adapter_name) in language_servers_to_stop { + self.stop_language_server(worktree_id, adapter_name, cx) + .detach(); + } + + for (worktree, language, settings) in language_formatters_to_check { + self.install_default_formatters(worktree, &language, &settings, cx) + .detach_and_log_err(cx); + } + + // Start all the newly-enabled language servers. + for (worktree, language) in language_servers_to_start { + let worktree_path = worktree.read(cx).abs_path(); + self.start_language_servers(&worktree, worktree_path, language, cx); + } + + // Restart all language servers with changed initialization options. + for (worktree, language) in language_servers_to_restart { + self.restart_language_servers(worktree, language, cx); + } + + if self.copilot_lsp_subscription.is_none() { + if let Some(copilot) = Copilot::global(cx) { + for buffer in self.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade() { + self.register_buffer_with_copilot(&buffer, cx); + } + } + self.copilot_lsp_subscription = Some(subscribe_for_copilot_events(&copilot, cx)); + } + } + + cx.notify(); + } + + pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { + self.opened_buffers + .get(&remote_id) + .and_then(|buffer| buffer.upgrade()) + } + + pub fn languages(&self) -> &Arc { + &self.languages + } + + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn user_store(&self) -> Handle { + self.user_store.clone() + } + + pub fn opened_buffers(&self, cx: &AppContext) -> Vec> { + self.opened_buffers + .values() + .filter_map(|b| b.upgrade()) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn has_open_buffer(&self, path: impl Into, cx: &AppContext) -> bool { + let path = path.into(); + if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { + self.opened_buffers.iter().any(|(_, buffer)| { + if let Some(buffer) = buffer.upgrade() { + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.worktree == worktree && file.path() == &path.path { + return true; + } + } + } + false + }) + } else { + false + } + } + + pub fn fs(&self) -> &Arc { + &self.fs + } + + pub fn remote_id(&self) -> Option { + match self.client_state.as_ref()? { + ProjectClientState::Local { remote_id, .. } + | ProjectClientState::Remote { remote_id, .. } => Some(*remote_id), + } + } + + pub fn replica_id(&self) -> ReplicaId { + match &self.client_state { + Some(ProjectClientState::Remote { replica_id, .. }) => *replica_id, + _ => 0, + } + } + + fn metadata_changed(&mut self, cx: &mut ModelContext) { + if let Some(ProjectClientState::Local { updates_tx, .. }) = &mut self.client_state { + updates_tx + .unbounded_send(LocalProjectUpdate::WorktreesChanged) + .ok(); + } + cx.notify(); + } + + pub fn collaborators(&self) -> &HashMap { + &self.collaborators + } + + pub fn host(&self) -> Option<&Collaborator> { + self.collaborators.values().find(|c| c.replica_id == 0) + } + + /// Collect all worktrees, including ones that don't appear in the project panel + pub fn worktrees<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + DoubleEndedIterator> { + self.worktrees + .iter() + .filter_map(move |worktree| worktree.upgrade()) + } + + /// Collect all user-visible worktrees, the ones that appear in the project panel + pub fn visible_worktrees<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + DoubleEndedIterator> { + self.worktrees.iter().filter_map(|worktree| { + worktree.upgrade().and_then(|worktree| { + if worktree.read(cx).is_visible() { + Some(worktree) + } else { + None + } + }) + }) + } + + pub fn worktree_root_names<'a>(&'a self, cx: &'a AppContext) -> impl Iterator { + self.visible_worktrees(cx) + .map(|tree| tree.read(cx).root_name()) + } + + pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option> { + self.worktrees(cx) + .find(|worktree| worktree.read(cx).id() == id) + } + + pub fn worktree_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.worktrees(cx) + .find(|worktree| worktree.read(cx).contains_entry(entry_id)) + } + + pub fn worktree_id_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option { + self.worktree_for_entry(entry_id, cx) + .map(|worktree| worktree.read(cx).id()) + } + + pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { + paths.iter().all(|path| self.contains_path(path, cx)) + } + + pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool { + for worktree in self.worktrees(cx) { + let worktree = worktree.read(cx).as_local(); + if worktree.map_or(false, |w| w.contains_abs_path(path)) { + return true; + } + } + false + } + + pub fn create_entry( + &mut self, + project_path: impl Into, + is_directory: bool, + cx: &mut ModelContext, + ) -> Option>> { + let project_path = project_path.into(); + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + if self.is_local() { + Some(worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .create_entry(project_path.path, is_directory, cx) + })) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CreateProjectEntry { + worktree_id: project_path.worktree_id.to_proto(), + project_id, + path: project_path.path.to_string_lossy().into(), + is_directory, + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn copy_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let new_path = new_path.into(); + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::CopyProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + new_path: new_path.to_string_lossy().into(), + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn rename_entry( + &mut self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let new_path = new_path.into(); + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .rename_entry(entry_id, new_path, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::RenameProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + new_path: new_path.to_string_lossy().into(), + }) + .await?; + let entry = response + .entry + .ok_or_else(|| anyhow!("missing entry in response"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_remote_mut().unwrap().insert_entry( + entry, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn delete_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_entry(entry_id, cx)?; + + cx.emit(Event::DeletedEntry(entry_id)); + + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().delete_entry(entry_id, cx) + }) + } else { + let client = self.client.clone(); + let project_id = self.remote_id().unwrap(); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = client + .request(proto::DeleteProjectEntry { + project_id, + entry_id: entry_id.to_proto(), + }) + .await?; + worktree + .update(&mut cx, move |worktree, cx| { + worktree.as_remote_mut().unwrap().delete_entry( + entry_id, + response.worktree_scan_id as usize, + cx, + ) + }) + .await + })) + } + } + + pub fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_id(worktree_id, cx)?; + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().expand_entry(entry_id, cx) + }) + } else { + let worktree = worktree.downgrade(); + let request = self.client.request(proto::ExpandProjectEntry { + project_id: self.remote_id().unwrap(), + entry_id: entry_id.to_proto(), + }); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = request.await?; + if let Some(worktree) = worktree.upgrade() { + worktree + .update(&mut cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .wait_for_snapshot(response.worktree_scan_id as usize) + }) + .await?; + } + Ok(()) + })) + } + } + + pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext) -> Result<()> { + if self.client_state.is_some() { + return Err(anyhow!("project was already shared")); + } + self.client_subscriptions.push( + self.client + .subscribe_to_entity(project_id)? + .set_model(&cx.handle(), &mut cx.to_async()), + ); + + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade() { + *open_buffer = OpenBuffer::Strong(buffer); + } + } + OpenBuffer::Operations(_) => unreachable!(), + } + } + + for worktree_handle in self.worktrees.iter_mut() { + match worktree_handle { + WorktreeHandle::Strong(_) => {} + WorktreeHandle::Weak(worktree) => { + if let Some(worktree) = worktree.upgrade() { + *worktree_handle = WorktreeHandle::Strong(worktree); + } + } + } + } + + for (server_id, status) in &self.language_server_statuses { + self.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.0 as u64, + name: status.name.clone(), + }), + }) + .log_err(); + } + + let store = cx.global::(); + for worktree in self.worktrees(cx) { + let worktree_id = worktree.read(cx).id().to_proto(); + for (path, content) in store.local_settings(worktree.id()) { + self.client + .send(proto::UpdateWorktreeSettings { + project_id, + worktree_id, + path: path.to_string_lossy().into(), + content: Some(content), + }) + .log_err(); + } + } + + let (updates_tx, mut updates_rx) = mpsc::unbounded(); + let client = self.client.clone(); + self.client_state = Some(ProjectClientState::Local { + remote_id: project_id, + updates_tx, + _send_updates: cx.spawn_weak(move |this, mut cx| async move { + while let Some(update) = updates_rx.next().await { + let Some(this) = this.upgrade(&cx) else { break }; + + match update { + LocalProjectUpdate::WorktreesChanged => { + let worktrees = this + .read_with(&cx, |this, cx| this.worktrees(cx).collect::>()); + let update_project = this + .read_with(&cx, |this, cx| { + this.client.request(proto::UpdateProject { + project_id, + worktrees: this.worktree_metadata_protos(cx), + }) + }) + .await; + if update_project.is_ok() { + for worktree in worktrees { + worktree.update(&mut cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree.share(project_id, cx).detach_and_log_err(cx) + }); + } + } + } + LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id } => { + let buffer = this.update(&mut cx, |this, _| { + let buffer = this.opened_buffers.get(&buffer_id).unwrap(); + let shared_buffers = + this.shared_buffers.entry(peer_id).or_default(); + if shared_buffers.insert(buffer_id) { + if let OpenBuffer::Strong(buffer) = buffer { + Some(buffer.clone()) + } else { + None + } + } else { + None + } + }); + + let Some(buffer) = buffer else { continue }; + let operations = + buffer.read_with(&cx, |b, cx| b.serialize_ops(None, cx)); + let operations = operations.await; + let state = buffer.read_with(&cx, |buffer, _| buffer.to_proto()); + + let initial_state = proto::CreateBufferForPeer { + project_id, + peer_id: Some(peer_id), + variant: Some(proto::create_buffer_for_peer::Variant::State(state)), + }; + if client.send(initial_state).log_err().is_some() { + let client = client.clone(); + cx.background() + .spawn(async move { + let mut chunks = split_operations(operations).peekable(); + while let Some(chunk) = chunks.next() { + let is_last = chunks.peek().is_none(); + client.send(proto::CreateBufferForPeer { + project_id, + peer_id: Some(peer_id), + variant: Some( + proto::create_buffer_for_peer::Variant::Chunk( + proto::BufferChunk { + buffer_id, + operations: chunk, + is_last, + }, + ), + ), + })?; + } + anyhow::Ok(()) + }) + .await + .log_err(); + } + } + } + } + }), + }); + + self.metadata_changed(cx); + cx.emit(Event::RemoteIdChanged(Some(project_id))); + cx.notify(); + Ok(()) + } + + pub fn reshared( + &mut self, + message: proto::ResharedProject, + cx: &mut ModelContext, + ) -> Result<()> { + self.shared_buffers.clear(); + self.set_collaborators_from_proto(message.collaborators, cx)?; + self.metadata_changed(cx); + Ok(()) + } + + pub fn rejoined( + &mut self, + message: proto::RejoinedProject, + message_id: u32, + cx: &mut ModelContext, + ) -> Result<()> { + cx.update_global::(|store, cx| { + for worktree in &self.worktrees { + store + .clear_local_settings(worktree.handle_id(), cx) + .log_err(); + } + }); + + self.join_project_response_message_id = message_id; + self.set_worktrees_from_proto(message.worktrees, cx)?; + self.set_collaborators_from_proto(message.collaborators, cx)?; + self.language_server_statuses = message + .language_servers + .into_iter() + .map(|server| { + ( + LanguageServerId(server.id as usize), + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ) + }) + .collect(); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::Resync) + .unwrap(); + cx.notify(); + Ok(()) + } + + pub fn unshare(&mut self, cx: &mut ModelContext) -> Result<()> { + self.unshare_internal(cx)?; + self.metadata_changed(cx); + cx.notify(); + Ok(()) + } + + fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> { + if self.is_remote() { + return Err(anyhow!("attempted to unshare a remote project")); + } + + if let Some(ProjectClientState::Local { remote_id, .. }) = self.client_state.take() { + self.collaborators.clear(); + self.shared_buffers.clear(); + self.client_subscriptions.clear(); + + for worktree_handle in self.worktrees.iter_mut() { + if let WorktreeHandle::Strong(worktree) = worktree_handle { + let is_visible = worktree.update(cx, |worktree, _| { + worktree.as_local_mut().unwrap().unshare(); + worktree.is_visible() + }); + if !is_visible { + *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); + } + } + } + + for open_buffer in self.opened_buffers.values_mut() { + // Wake up any tasks waiting for peers' edits to this buffer. + if let Some(buffer) = open_buffer.upgrade() { + buffer.update(cx, |buffer, _| buffer.give_up_waiting()); + } + + if let OpenBuffer::Strong(buffer) = open_buffer { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + } + + self.client.send(proto::UnshareProject { + project_id: remote_id, + })?; + + Ok(()) + } else { + Err(anyhow!("attempted to unshare an unshared project")) + } + } + + pub fn disconnected_from_host(&mut self, cx: &mut ModelContext) { + self.disconnected_from_host_internal(cx); + cx.emit(Event::DisconnectedFromHost); + cx.notify(); + } + + fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) { + if let Some(ProjectClientState::Remote { + sharing_has_stopped, + .. + }) = &mut self.client_state + { + *sharing_has_stopped = true; + + self.collaborators.clear(); + + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade() { + worktree.update(cx, |worktree, _| { + if let Some(worktree) = worktree.as_remote_mut() { + worktree.disconnected_from_host(); + } + }); + } + } + + for open_buffer in self.opened_buffers.values_mut() { + // Wake up any tasks waiting for peers' edits to this buffer. + if let Some(buffer) = open_buffer.upgrade() { + buffer.update(cx, |buffer, _| buffer.give_up_waiting()); + } + + if let OpenBuffer::Strong(buffer) = open_buffer { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + } + + // Wake up all futures currently waiting on a buffer to get opened, + // to give them a chance to fail now that we've disconnected. + *self.opened_buffer.0.borrow_mut() = (); + } + } + + pub fn close(&mut self, cx: &mut ModelContext) { + cx.emit(Event::Closed); + } + + pub fn is_read_only(&self) -> bool { + match &self.client_state { + Some(ProjectClientState::Remote { + sharing_has_stopped, + .. + }) => *sharing_has_stopped, + _ => false, + } + } + + pub fn is_local(&self) -> bool { + match &self.client_state { + Some(ProjectClientState::Remote { .. }) => false, + _ => true, + } + } + + pub fn is_remote(&self) -> bool { + !self.is_local() + } + + pub fn create_buffer( + &mut self, + text: &str, + language: Option>, + cx: &mut ModelContext, + ) -> Result> { + if self.is_remote() { + return Err(anyhow!("creating buffers as a guest is not supported yet")); + } + let id = post_inc(&mut self.next_buffer_id); + let buffer = cx.add_model(|cx| { + Buffer::new(self.replica_id(), id, text).with_language( + language.unwrap_or_else(|| language2::PLAIN_TEXT.clone()), + cx, + ) + }); + self.register_buffer(&buffer, cx)?; + Ok(buffer) + } + + pub fn open_path( + &mut self, + path: impl Into, + cx: &mut ModelContext, + ) -> Task> { + let task = self.open_buffer(path, cx); + cx.spawn_weak(|_, cx| async move { + let buffer = task.await?; + let project_entry_id = buffer + .read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + }) + .ok_or_else(|| anyhow!("no project entry"))?; + + let buffer: &AnyHandle = &buffer; + Ok((project_entry_id, buffer.clone())) + }) + } + + pub fn open_local_buffer( + &mut self, + abs_path: impl AsRef, + cx: &mut ModelContext, + ) -> Task>> { + if let Some((worktree, relative_path)) = self.find_local_worktree(abs_path.as_ref(), cx) { + self.open_buffer((worktree.read(cx).id(), relative_path), cx) + } else { + Task::ready(Err(anyhow!("no such path"))) + } + } + + pub fn open_buffer( + &mut self, + path: impl Into, + cx: &mut ModelContext, + ) -> Task>> { + let project_path = path.into(); + let worktree = if let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) { + worktree + } else { + return Task::ready(Err(anyhow!("no such worktree"))); + }; + + // If there is already a buffer for the given path, then return it. + let existing_buffer = self.get_open_buffer(&project_path, cx); + if let Some(existing_buffer) = existing_buffer { + return Task::ready(Ok(existing_buffer)); + } + + let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) { + // If the given path is already being loaded, then wait for that existing + // task to complete and return the same buffer. + hash_map::Entry::Occupied(e) => e.get().clone(), + + // Otherwise, record the fact that this path is now being loaded. + hash_map::Entry::Vacant(entry) => { + let (mut tx, rx) = postage::watch::channel(); + entry.insert(rx.clone()); + + let load_buffer = if worktree.read(cx).is_local() { + self.open_local_buffer_internal(&project_path.path, &worktree, cx) + } else { + self.open_remote_buffer_internal(&project_path.path, &worktree, cx) + }; + + cx.spawn(move |this, mut cx| async move { + let load_result = load_buffer.await; + *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| { + // Record the fact that the buffer is no longer loading. + this.loading_buffers_by_path.remove(&project_path); + let buffer = load_result.map_err(Arc::new)?; + Ok(buffer) + })); + }) + .detach(); + rx + } + }; + + cx.foreground().spawn(async move { + wait_for_loading_buffer(loading_watch) + .await + .map_err(|error| anyhow!("{}", error)) + }) + } + + fn open_local_buffer_internal( + &mut self, + path: &Arc, + worktree: &Handle, + cx: &mut ModelContext, + ) -> Task>> { + let buffer_id = post_inc(&mut self.next_buffer_id); + let load_buffer = worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree.load_buffer(buffer_id, path, cx) + }); + cx.spawn(|this, mut cx| async move { + let buffer = load_buffer.await?; + this.update(&mut cx, |this, cx| this.register_buffer(&buffer, cx))?; + Ok(buffer) + }) + } + + fn open_remote_buffer_internal( + &mut self, + path: &Arc, + worktree: &Handle, + cx: &mut ModelContext, + ) -> Task>> { + let rpc = self.client.clone(); + let project_id = self.remote_id().unwrap(); + let remote_worktree_id = worktree.read(cx).id(); + let path = path.clone(); + let path_string = path.to_string_lossy().to_string(); + cx.spawn(|this, mut cx| async move { + let response = rpc + .request(proto::OpenBufferByPath { + project_id, + worktree_id: remote_worktree_id.to_proto(), + path: path_string, + }) + .await?; + this.update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(response.buffer_id, cx) + }) + .await + }) + } + + /// LanguageServerName is owned, because it is inserted into a map + pub fn open_local_buffer_via_lsp( + &mut self, + abs_path: lsp2::Url, + language_server_id: LanguageServerId, + language_server_name: LanguageServerName, + cx: &mut ModelContext, + ) -> Task>> { + cx.spawn(|this, mut cx| async move { + let abs_path = abs_path + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let (worktree, relative_path) = if let Some(result) = + this.read_with(&cx, |this, cx| this.find_local_worktree(&abs_path, cx)) + { + result + } else { + let worktree = this + .update(&mut cx, |this, cx| { + this.create_local_worktree(&abs_path, false, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.language_server_ids.insert( + (worktree.read(cx).id(), language_server_name), + language_server_id, + ); + }); + (worktree, PathBuf::new()) + }; + + let project_path = ProjectPath { + worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), + path: relative_path.into(), + }; + this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await + }) + } + + pub fn open_buffer_by_id( + &mut self, + id: u64, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(buffer) = self.buffer_for_id(id, cx) { + Task::ready(Ok(buffer)) + } else if self.is_local() { + Task::ready(Err(anyhow!("buffer {} does not exist", id))) + } else if let Some(project_id) = self.remote_id() { + let request = self + .client + .request(proto::OpenBufferById { project_id, id }); + cx.spawn(|this, mut cx| async move { + let buffer_id = request.await?.buffer_id; + this.update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(buffer_id, cx) + }) + .await + }) + } else { + Task::ready(Err(anyhow!("cannot open buffer while disconnected"))) + } + } + + pub fn save_buffers( + &self, + buffers: HashSet>, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + let save_tasks = buffers + .into_iter() + .map(|buffer| this.update(&mut cx, |this, cx| this.save_buffer(buffer, cx))); + try_join_all(save_tasks).await?; + Ok(()) + }) + } + + pub fn save_buffer( + &self, + buffer: Handle, + cx: &mut ModelContext, + ) -> Task> { + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return Task::ready(Err(anyhow!("buffer doesn't have a file"))); + }; + let worktree = file.worktree.clone(); + let path = file.path.clone(); + worktree.update(cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx), + Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx), + }) + } + + pub fn save_buffer_as( + &mut self, + buffer: Handle, + abs_path: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx); + let old_file = File::from_dyn(buffer.read(cx).file()) + .filter(|f| f.is_local()) + .cloned(); + cx.spawn(|this, mut cx| async move { + if let Some(old_file) = &old_file { + this.update(&mut cx, |this, cx| { + this.unregister_buffer_from_language_servers(&buffer, old_file, cx); + }); + } + let (worktree, path) = worktree_task.await?; + worktree + .update(&mut cx, |worktree, cx| match worktree { + Worktree::Local(worktree) => { + worktree.save_buffer(buffer.clone(), path.into(), true, cx) + } + Worktree::Remote(_) => panic!("cannot remote buffers as new files"), + }) + .await?; + + this.update(&mut cx, |this, cx| { + this.detect_language_for_buffer(&buffer, cx); + this.register_buffer_with_language_servers(&buffer, cx); + }); + Ok(()) + }) + } + + pub fn get_open_buffer( + &mut self, + path: &ProjectPath, + cx: &mut ModelContext, + ) -> Option> { + let worktree = self.worktree_for_id(path.worktree_id, cx)?; + self.opened_buffers.values().find_map(|buffer| { + let buffer = buffer.upgrade()?; + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree == worktree && file.path() == &path.path { + Some(buffer) + } else { + None + } + }) + } + + fn register_buffer( + &mut self, + buffer: &Handle, + cx: &mut ModelContext, + ) -> Result<()> { + self.request_buffer_diff_recalculation(buffer, cx); + buffer.update(cx, |buffer, _| { + buffer.set_language_registry(self.languages.clone()) + }); + + let remote_id = buffer.read(cx).remote_id(); + let is_remote = self.is_remote(); + let open_buffer = if is_remote || self.is_shared() { + OpenBuffer::Strong(buffer.clone()) + } else { + OpenBuffer::Weak(buffer.downgrade()) + }; + + match self.opened_buffers.entry(remote_id) { + hash_map::Entry::Vacant(entry) => { + entry.insert(open_buffer); + } + hash_map::Entry::Occupied(mut entry) => { + if let OpenBuffer::Operations(operations) = entry.get_mut() { + buffer.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx))?; + } else if entry.get().upgrade().is_some() { + if is_remote { + return Ok(()); + } else { + debug_panic!("buffer {} was already registered", remote_id); + Err(anyhow!("buffer {} was already registered", remote_id))?; + } + } + entry.insert(open_buffer); + } + } + cx.subscribe(buffer, |this, buffer, event, cx| { + this.on_buffer_event(buffer, event, cx); + }) + .detach(); + + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.is_local { + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); + + self.local_buffer_ids_by_entry_id + .insert(file.entry_id, remote_id); + } + } + + self.detect_language_for_buffer(buffer, cx); + self.register_buffer_with_language_servers(buffer, cx); + self.register_buffer_with_copilot(buffer, cx); + cx.observe_release(buffer, |this, buffer, cx| { + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap(); + for server in this.language_servers_for_buffer(buffer, cx) { + server + .1 + .notify::( + lsp2::DidCloseTextDocumentParams { + text_document: lsp2::TextDocumentIdentifier::new(uri.clone()), + }, + ) + .log_err(); + } + } + } + }) + .detach(); + + *self.opened_buffer.0.borrow_mut() = (); + Ok(()) + } + + fn register_buffer_with_language_servers( + &mut self, + buffer_handle: &Handle, + cx: &mut ModelContext, + ) { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + + if let Some(file) = File::from_dyn(buffer.file()) { + if !file.is_local() { + return; + } + + let abs_path = file.abs_path(cx); + let uri = lsp2::Url::from_file_path(&abs_path) + .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}")); + let initial_snapshot = buffer.text_snapshot(); + let language = buffer.language().cloned(); + let worktree_id = file.worktree_id(cx); + + if let Some(local_worktree) = file.worktree.read(cx).as_local() { + for (server_id, diagnostics) in local_worktree.diagnostics_for_path(file.path()) { + self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx) + .log_err(); + } + } + + if let Some(language) = language { + for adapter in language.lsp_adapters() { + let language_id = adapter.language_ids.get(language.name().as_ref()).cloned(); + let server = self + .language_server_ids + .get(&(worktree_id, adapter.name.clone())) + .and_then(|id| self.language_servers.get(id)) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); + let server = match server { + Some(server) => server, + None => continue, + }; + + server + .notify::( + lsp2::DidOpenTextDocumentParams { + text_document: lsp2::TextDocumentItem::new( + uri.clone(), + language_id.unwrap_or_default(), + 0, + initial_snapshot.text(), + ), + }, + ) + .log_err(); + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| provider.trigger_characters.clone()) + .unwrap_or_default(), + cx, + ); + }); + + let snapshot = LspBufferSnapshot { + version: 0, + snapshot: initial_snapshot.clone(), + }; + self.buffer_snapshots + .entry(buffer_id) + .or_default() + .insert(server.server_id(), vec![snapshot]); + } + } + } + } + + fn unregister_buffer_from_language_servers( + &mut self, + buffer: &Handle, + old_file: &File, + cx: &mut ModelContext, + ) { + let old_path = match old_file.as_local() { + Some(local) => local.abs_path(cx), + None => return, + }; + + buffer.update(cx, |buffer, cx| { + let worktree_id = old_file.worktree_id(cx); + let ids = &self.language_server_ids; + + let language = buffer.language().cloned(); + let adapters = language.iter().flat_map(|language| language.lsp_adapters()); + for &server_id in adapters.flat_map(|a| ids.get(&(worktree_id, a.name.clone()))) { + buffer.update_diagnostics(server_id, Default::default(), cx); + } + + self.buffer_snapshots.remove(&buffer.remote_id()); + let file_url = lsp2::Url::from_file_path(old_path).unwrap(); + for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { + language_server + .notify::( + lsp2::DidCloseTextDocumentParams { + text_document: lsp2::TextDocumentIdentifier::new(file_url.clone()), + }, + ) + .log_err(); + } + }); + } + + fn register_buffer_with_copilot( + &self, + buffer_handle: &Handle, + cx: &mut ModelContext, + ) { + if let Some(copilot) = Copilot::global(cx) { + copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx)); + } + } + + async fn send_buffer_ordered_messages( + this: WeakHandle, + rx: UnboundedReceiver, + mut cx: AsyncAppContext, + ) -> Option<()> { + const MAX_BATCH_SIZE: usize = 128; + + let mut operations_by_buffer_id = HashMap::default(); + async fn flush_operations( + this: &Handle, + operations_by_buffer_id: &mut HashMap>, + needs_resync_with_host: &mut bool, + is_local: bool, + cx: &AsyncAppContext, + ) { + for (buffer_id, operations) in operations_by_buffer_id.drain() { + let request = this.read_with(cx, |this, _| { + let project_id = this.remote_id()?; + Some(this.client.request(proto::UpdateBuffer { + buffer_id, + project_id, + operations, + })) + }); + if let Some(request) = request { + if request.await.is_err() && !is_local { + *needs_resync_with_host = true; + break; + } + } + } + } + + let mut needs_resync_with_host = false; + let mut changes = rx.ready_chunks(MAX_BATCH_SIZE); + + while let Some(changes) = changes.next().await { + let this = this.upgrade()?; + let is_local = this.read_with(&cx, |this, _| this.is_local()); + + for change in changes { + match change { + BufferOrderedMessage::Operation { + buffer_id, + operation, + } => { + if needs_resync_with_host { + continue; + } + + operations_by_buffer_id + .entry(buffer_id) + .or_insert(Vec::new()) + .push(operation); + } + + BufferOrderedMessage::Resync => { + operations_by_buffer_id.clear(); + if this + .update(&mut cx, |this, cx| this.synchronize_remote_buffers(cx)) + .await + .is_ok() + { + needs_resync_with_host = false; + } + } + + BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message, + } => { + flush_operations( + &this, + &mut operations_by_buffer_id, + &mut needs_resync_with_host, + is_local, + &cx, + ) + .await; + + this.read_with(&cx, |this, _| { + if let Some(project_id) = this.remote_id() { + this.client + .send(proto::UpdateLanguageServer { + project_id, + language_server_id: language_server_id.0 as u64, + variant: Some(message), + }) + .log_err(); + } + }); + } + } + } + + flush_operations( + &this, + &mut operations_by_buffer_id, + &mut needs_resync_with_host, + is_local, + &cx, + ) + .await; + } + + None + } + + fn on_buffer_event( + &mut self, + buffer: Handle, + event: &BufferEvent, + cx: &mut ModelContext, + ) -> Option<()> { + if matches!( + event, + BufferEvent::Edited { .. } | BufferEvent::Reloaded | BufferEvent::DiffBaseChanged + ) { + self.request_buffer_diff_recalculation(&buffer, cx); + } + + match event { + BufferEvent::Operation(operation) => { + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::Operation { + buffer_id: buffer.read(cx).remote_id(), + operation: language2::proto::serialize_operation(operation), + }) + .ok(); + } + + BufferEvent::Edited { .. } => { + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; + let abs_path = file.as_local()?.abs_path(cx); + let uri = lsp2::Url::from_file_path(abs_path).unwrap(); + let next_snapshot = buffer.text_snapshot(); + + let language_servers: Vec<_> = self + .language_servers_for_buffer(buffer, cx) + .map(|i| i.1.clone()) + .collect(); + + for language_server in language_servers { + let language_server = language_server.clone(); + + let buffer_snapshots = self + .buffer_snapshots + .get_mut(&buffer.remote_id()) + .and_then(|m| m.get_mut(&language_server.server_id()))?; + let previous_snapshot = buffer_snapshots.last()?; + + let build_incremental_change = || { + buffer + .edits_since::<(PointUtf16, usize)>( + previous_snapshot.snapshot.version(), + ) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = next_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp2::TextDocumentContentChangeEvent { + range: Some(lsp2::Range::new( + point_to_lsp(edit_start), + point_to_lsp(edit_end), + )), + range_length: None, + text: new_text, + } + }) + .collect() + }; + + let document_sync_kind = language_server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp2::TextDocumentSyncCapability::Kind(kind) => Some(*kind), + lsp2::TextDocumentSyncCapability::Options(options) => options.change, + }); + + let content_changes: Vec<_> = match document_sync_kind { + Some(lsp2::TextDocumentSyncKind::FULL) => { + vec![lsp2::TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: next_snapshot.text(), + }] + } + Some(lsp2::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), + _ => { + #[cfg(any(test, feature = "test-support"))] + { + build_incremental_change() + } + + #[cfg(not(any(test, feature = "test-support")))] + { + continue; + } + } + }; + + let next_version = previous_snapshot.version + 1; + + buffer_snapshots.push(LspBufferSnapshot { + version: next_version, + snapshot: next_snapshot.clone(), + }); + + language_server + .notify::( + lsp2::DidChangeTextDocumentParams { + text_document: lsp2::VersionedTextDocumentIdentifier::new( + uri.clone(), + next_version, + ), + content_changes, + }, + ) + .log_err(); + } + } + + BufferEvent::Saved => { + let file = File::from_dyn(buffer.read(cx).file())?; + let worktree_id = file.worktree_id(cx); + let abs_path = file.as_local()?.abs_path(cx); + let text_document = lsp2::TextDocumentIdentifier { + uri: lsp2::Url::from_file_path(abs_path).unwrap(), + }; + + for (_, _, server) in self.language_servers_for_worktree(worktree_id) { + let text = include_text(server.as_ref()).then(|| buffer.read(cx).text()); + + server + .notify::( + lsp2::DidSaveTextDocumentParams { + text_document: text_document.clone(), + text, + }, + ) + .log_err(); + } + + let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx); + for language_server_id in language_server_ids { + if let Some(LanguageServerState::Running { + adapter, + simulate_disk_based_diagnostics_completion, + .. + }) = self.language_servers.get_mut(&language_server_id) + { + // After saving a buffer using a language server that doesn't provide + // a disk-based progress token, kick off a timer that will reset every + // time the buffer is saved. If the timer eventually fires, simulate + // disk-based diagnostics being finished so that other pieces of UI + // (e.g., project diagnostics view, diagnostic status bar) can update. + // We don't emit an event right away because the language server might take + // some time to publish diagnostics. + if adapter.disk_based_diagnostics_progress_token.is_none() { + const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = + Duration::from_secs(1); + + let task = cx.spawn_weak(|this, mut cx| async move { + cx.background().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_finished( + language_server_id, + cx, + ); + this.buffer_ordered_messages_tx + .unbounded_send( + BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default()) + }, + ) + .ok(); + }); + } + }); + *simulate_disk_based_diagnostics_completion = Some(task); + } + } + } + } + BufferEvent::FileHandleChanged => { + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return None; + }; + + match self.local_buffer_ids_by_entry_id.get(&file.entry_id) { + Some(_) => { + return None; + } + None => { + let remote_id = buffer.read(cx).remote_id(); + self.local_buffer_ids_by_entry_id + .insert(file.entry_id, remote_id); + + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + remote_id, + ); + } + } + } + _ => {} + } + + None + } + + fn request_buffer_diff_recalculation( + &mut self, + buffer: &Handle, + cx: &mut ModelContext, + ) { + self.buffers_needing_diff.insert(buffer.downgrade()); + let first_insertion = self.buffers_needing_diff.len() == 1; + + let settings = settings2::get::(cx); + let delay = if let Some(delay) = settings.git.gutter_debounce { + delay + } else { + if first_insertion { + let this = cx.weak_handle(); + cx.defer(move |cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + this.recalculate_buffer_diffs(cx).detach(); + }); + } + }); + } + return; + }; + + const MIN_DELAY: u64 = 50; + let delay = delay.max(MIN_DELAY); + let duration = Duration::from_millis(delay); + + self.git_diff_debouncer + .fire_new(duration, cx, move |this, cx| { + this.recalculate_buffer_diffs(cx) + }); + } + + fn recalculate_buffer_diffs(&mut self, cx: &mut ModelContext) -> Task<()> { + cx.spawn(|this, mut cx| async move { + let buffers: Vec<_> = this.update(&mut cx, |this, _| { + this.buffers_needing_diff.drain().collect() + }); + + let tasks: Vec<_> = this.update(&mut cx, |_, cx| { + buffers + .iter() + .filter_map(|buffer| { + let buffer = buffer.upgrade()?; + buffer.update(cx, |buffer, cx| buffer.git_diff_recalc(cx)) + }) + .collect() + }); + + futures::future::join_all(tasks).await; + + this.update(&mut cx, |this, cx| { + if !this.buffers_needing_diff.is_empty() { + this.recalculate_buffer_diffs(cx).detach(); + } else { + // TODO: Would a `ModelContext.notify()` suffice here? + for buffer in buffers { + if let Some(buffer) = buffer.upgrade() { + buffer.update(cx, |_, cx| cx.notify()); + } + } + } + }); + }) + } + + fn language_servers_for_worktree( + &self, + worktree_id: WorktreeId, + ) -> impl Iterator, &Arc, &Arc)> { + self.language_server_ids + .iter() + .filter_map(move |((language_server_worktree_id, _), id)| { + if *language_server_worktree_id == worktree_id { + if let Some(LanguageServerState::Running { + adapter, + language, + server, + .. + }) = self.language_servers.get(id) + { + return Some((adapter, language, server)); + } + } + None + }) + } + + fn maintain_buffer_languages( + languages: Arc, + cx: &mut ModelContext, + ) -> Task<()> { + let mut subscription = languages.subscribe(); + let mut prev_reload_count = languages.reload_count(); + cx.spawn_weak(|project, mut cx| async move { + while let Some(()) = subscription.next().await { + if let Some(project) = project.upgrade(&cx) { + // If the language registry has been reloaded, then remove and + // re-assign the languages on all open buffers. + let reload_count = languages.reload_count(); + if reload_count > prev_reload_count { + prev_reload_count = reload_count; + project.update(&mut cx, |this, cx| { + let buffers = this + .opened_buffers + .values() + .filter_map(|b| b.upgrade()) + .collect::>(); + for buffer in buffers { + if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() { + this.unregister_buffer_from_language_servers(&buffer, &f, cx); + buffer.update(cx, |buffer, cx| buffer.set_language(None, cx)); + } + } + }); + } + + project.update(&mut cx, |project, cx| { + let mut plain_text_buffers = Vec::new(); + let mut buffers_with_unknown_injections = Vec::new(); + for buffer in project.opened_buffers.values() { + if let Some(handle) = buffer.upgrade() { + let buffer = &handle.read(cx); + if buffer.language().is_none() + || buffer.language() == Some(&*language2::PLAIN_TEXT) + { + plain_text_buffers.push(handle); + } else if buffer.contains_unknown_injections() { + buffers_with_unknown_injections.push(handle); + } + } + } + + for buffer in plain_text_buffers { + project.detect_language_for_buffer(&buffer, cx); + project.register_buffer_with_language_servers(&buffer, cx); + } + + for buffer in buffers_with_unknown_injections { + buffer.update(cx, |buffer, cx| buffer.reparse(cx)); + } + }); + } + } + }) + } + + fn maintain_workspace_config(cx: &mut ModelContext) -> Task<()> { + let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); + let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); + + let settings_observation = cx.observe_global::(move |_, _| { + *settings_changed_tx.borrow_mut() = (); + }); + + cx.spawn_weak(|this, mut cx| async move { + while let Some(_) = settings_changed_rx.next().await { + let Some(this) = this.upgrade(&cx) else { + break; + }; + + let servers: Vec<_> = this.read_with(&cx, |this, _| { + this.language_servers + .values() + .filter_map(|state| match state { + LanguageServerState::Starting(_) => None, + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter.clone(), server.clone())), + }) + .collect() + }); + + for (adapter, server) in servers { + let workspace_config = + cx.update(|cx| adapter.workspace_configuration(cx)).await; + server + .notify::( + lsp2::DidChangeConfigurationParams { + settings: workspace_config.clone(), + }, + ) + .ok(); + } + } + + drop(settings_observation); + }) + } + + fn detect_language_for_buffer( + &mut self, + buffer_handle: &Handle, + cx: &mut ModelContext, + ) -> Option<()> { + // If the buffer has a language, set it and start the language server if we haven't already. + let buffer = buffer_handle.read(cx); + let full_path = buffer.file()?.full_path(cx); + let content = buffer.as_rope(); + let new_language = self + .languages + .language_for_file(&full_path, Some(content)) + .now_or_never()? + .ok()?; + self.set_language_for_buffer(buffer_handle, new_language, cx); + None + } + + pub fn set_language_for_buffer( + &mut self, + buffer: &Handle, + new_language: Arc, + cx: &mut ModelContext, + ) { + buffer.update(cx, |buffer, cx| { + if buffer.language().map_or(true, |old_language| { + !Arc::ptr_eq(old_language, &new_language) + }) { + buffer.set_language(Some(new_language.clone()), cx); + } + }); + + let buffer_file = buffer.read(cx).file().cloned(); + let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); + let buffer_file = File::from_dyn(buffer_file.as_ref()); + let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); + + let task_buffer = buffer.clone(); + let prettier_installation_task = + self.install_default_formatters(worktree, &new_language, &settings, cx); + cx.spawn(|project, mut cx| async move { + prettier_installation_task.await?; + let _ = project + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(&task_buffer, cx) + }) + .await; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + if let Some(file) = buffer_file { + let worktree = file.worktree.clone(); + if let Some(tree) = worktree.read(cx).as_local() { + self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); + } + } + } + + fn start_language_servers( + &mut self, + worktree: &Handle, + worktree_path: Arc, + language: Arc, + cx: &mut ModelContext, + ) { + let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx)); + let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx); + if !settings.enable_language_server { + return; + } + + let worktree_id = worktree.read(cx).id(); + for adapter in language.lsp_adapters() { + self.start_language_server( + worktree_id, + worktree_path.clone(), + adapter.clone(), + language.clone(), + cx, + ); + } + } + + fn start_language_server( + &mut self, + worktree_id: WorktreeId, + worktree_path: Arc, + adapter: Arc, + language: Arc, + cx: &mut ModelContext, + ) { + let key = (worktree_id, adapter.name.clone()); + if self.language_server_ids.contains_key(&key) { + return; + } + + let pending_server = match self.languages.create_pending_language_server( + language.clone(), + adapter.clone(), + worktree_path, + ProjectLspAdapterDelegate::new(self, cx), + cx, + ) { + Some(pending_server) => pending_server, + None => return, + }; + + let project_settings = settings2::get::(cx); + let lsp = project_settings.lsp2.get(&adapter.name.0); + let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); + + let mut initialization_options = adapter.initialization_options.clone(); + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } + + let server_id = pending_server.server_id; + let container_dir = pending_server.container_dir.clone(); + let state = LanguageServerState::Starting({ + let adapter = adapter.clone(); + let server_name = adapter.name.0.clone(); + let language = language.clone(); + let key = key.clone(); + + cx.spawn_weak(|this, mut cx| async move { + let result = Self::setup_and_insert_language_server( + this, + initialization_options, + pending_server, + adapter.clone(), + language.clone(), + server_id, + key, + &mut cx, + ) + .await; + + match result { + Ok(server) => server, + + Err(err) => { + log::error!("failed to start language server {:?}: {}", server_name, err); + + if let Some(this) = this.upgrade() { + if let Some(container_dir) = container_dir { + let installation_test_binary = adapter + .installation_test_binary(container_dir.to_path_buf()) + .await; + + this.update(&mut cx, |_, cx| { + Self::check_errored_server( + language, + adapter, + server_id, + installation_test_binary, + cx, + ) + }); + } + } + + None + } + } + }) + }); + + self.language_servers.insert(server_id, state); + self.language_server_ids.insert(key, server_id); + } + + fn reinstall_language_server( + &mut self, + language: Arc, + adapter: Arc, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option> { + log::info!("beginning to reinstall server"); + + let existing_server = match self.language_servers.remove(&server_id) { + Some(LanguageServerState::Running { server, .. }) => Some(server), + _ => None, + }; + + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade() { + let key = (worktree.read(cx).id(), adapter.name.clone()); + self.language_server_ids.remove(&key); + } + } + + Some(cx.spawn(move |this, mut cx| async move { + if let Some(task) = existing_server.and_then(|server| server.shutdown()) { + log::info!("shutting down existing server"); + task.await; + } + + // TODO: This is race-safe with regards to preventing new instances from + // starting while deleting, but existing instances in other projects are going + // to be very confused and messed up + this.update(&mut cx, |this, cx| { + this.languages.delete_server_container(adapter.clone(), cx) + }) + .await; + + this.update(&mut cx, |this, mut cx| { + let worktrees = this.worktrees.clone(); + for worktree in worktrees { + let worktree = match worktree.upgrade() { + Some(worktree) => worktree.read(cx), + None => continue, + }; + let worktree_id = worktree.id(); + let root_path = worktree.abs_path(); + + this.start_language_server( + worktree_id, + root_path, + adapter.clone(), + language.clone(), + &mut cx, + ); + } + }) + })) + } + + async fn setup_and_insert_language_server( + this: WeakHandle, + initialization_options: Option, + pending_server: PendingLanguageServer, + adapter: Arc, + language: Arc, + server_id: LanguageServerId, + key: (WorktreeId, LanguageServerName), + cx: &mut AsyncAppContext, + ) -> Result>> { + let setup = Self::setup_pending_language_server( + this, + initialization_options, + pending_server, + adapter.clone(), + server_id, + cx, + ); + + let language_server = match setup.await? { + Some(language_server) => language_server, + None => return Ok(None), + }; + let this = match this.upgrade() { + Some(this) => this, + None => return Err(anyhow!("failed to upgrade project handle")), + }; + + this.update(cx, |this, cx| { + this.insert_newly_running_language_server( + language, + adapter, + language_server.clone(), + server_id, + key, + cx, + ) + })?; + + Ok(Some(language_server)) + } + + async fn setup_pending_language_server( + this: WeakHandle, + initialization_options: Option, + pending_server: PendingLanguageServer, + adapter: Arc, + server_id: LanguageServerId, + cx: &mut AsyncAppContext, + ) -> Result>> { + let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; + let language_server = match pending_server.task.await? { + Some(server) => server, + None => return Ok(None), + }; + + language_server + .on_notification::({ + let adapter = adapter.clone(); + move |mut params, mut cx| { + let this = this; + let adapter = adapter.clone(); + adapter.process_diagnostics(&mut params); + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.update_diagnostics( + server_id, + params, + &adapter.disk_based_diagnostic_sources, + cx, + ) + .log_err(); + }); + } + } + }) + .detach(); + + language_server + .on_request::({ + let adapter = adapter.clone(); + move |params, mut cx| { + let adapter = adapter.clone(); + async move { + let workspace_config = + cx.update(|cx| adapter.workspace_configuration(cx)).await; + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + workspace_config + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + workspace_config.clone() + } + }) + .collect()) + } + } + }) + .detach(); + + // Even though we don't have handling for these requests, respond to them to + // avoid stalling any language server like `gopls` which waits for a response + // to these requests when initializing. + language_server + .on_request::( + move |params, mut cx| async move { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + if let Some(status) = this.language_server_statuses.get_mut(&server_id) + { + if let lsp2::NumberOrString::String(token) = params.token { + status.progress_tokens.insert(token); + } + } + }); + } + Ok(()) + }, + ) + .detach(); + language_server + .on_request::({ + move |params, mut cx| async move { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + for reg in params.registrations { + if reg.method == "workspace/didChangeWatchedFiles" { + if let Some(options) = reg.register_options { + let options = serde_json::from_value(options)?; + this.update(&mut cx, |this, cx| { + this.on_lsp_did_change_watched_files(server_id, options, cx); + }); + } + } + } + Ok(()) + } + }) + .detach(); + + language_server + .on_request::({ + let adapter = adapter.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx) + } + }) + .detach(); + + language_server + .on_request::({ + move |(), mut cx| async move { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + this.update(&mut cx, |project, cx| { + cx.emit(Event::RefreshInlayHints); + project.remote_id().map(|project_id| { + project.client.send(proto::RefreshInlayHints { project_id }) + }) + }) + .transpose()?; + Ok(()) + } + }) + .detach(); + + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token.clone(); + + language_server + .on_notification::(move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token.clone(), + cx, + ); + }); + } + }) + .detach(); + + let language_server = language_server.initialize(initialization_options).await?; + + language_server + .notify::( + lsp2::DidChangeConfigurationParams { + settings: workspace_config, + }, + ) + .ok(); + + Ok(Some(language_server)) + } + + fn insert_newly_running_language_server( + &mut self, + language: Arc, + adapter: Arc, + language_server: Arc, + server_id: LanguageServerId, + key: (WorktreeId, LanguageServerName), + cx: &mut ModelContext, + ) -> Result<()> { + // If the language server for this key doesn't match the server id, don't store the + // server. Which will cause it to be dropped, killing the process + if self + .language_server_ids + .get(&key) + .map(|id| id != &server_id) + .unwrap_or(false) + { + return Ok(()); + } + + // Update language_servers collection with Running variant of LanguageServerState + // indicating that the server is up and running and ready + self.language_servers.insert( + server_id, + LanguageServerState::Running { + adapter: adapter.clone(), + language: language.clone(), + watched_paths: Default::default(), + server: language_server.clone(), + simulate_disk_based_diagnostics_completion: None, + }, + ); + + self.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ); + + cx.emit(Event::LanguageServerAdded(server_id)); + + if let Some(project_id) = self.remote_id() { + self.client.send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.0 as u64, + name: language_server.name().to_string(), + }), + })?; + } + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in self.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade() { + let buffer = buffer_handle.read(cx); + let file = match File::from_dyn(buffer.file()) { + Some(file) => file, + None => continue, + }; + let language = match buffer.language() { + Some(language) => language, + None => continue, + }; + + if file.worktree.read(cx).id() != key.0 + || !language.lsp_adapters().iter().any(|a| a.name == key.1) + { + continue; + } + + let file = match file.as_local() { + Some(file) => file, + None => continue, + }; + + let versions = self + .buffer_snapshots + .entry(buffer.remote_id()) + .or_default() + .entry(server_id) + .or_insert_with(|| { + vec![LspBufferSnapshot { + version: 0, + snapshot: buffer.text_snapshot(), + }] + }); + + let snapshot = versions.last().unwrap(); + let version = snapshot.version; + let initial_snapshot = &snapshot.snapshot; + let uri = lsp2::Url::from_file_path(file.abs_path(cx)).unwrap(); + language_server.notify::( + lsp2::DidOpenTextDocumentParams { + text_document: lsp2::TextDocumentItem::new( + uri, + adapter + .language_ids + .get(language.name().as_ref()) + .cloned() + .unwrap_or_default(), + version, + initial_snapshot.text(), + ), + }, + )?; + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| provider.trigger_characters.clone()) + .unwrap_or_default(), + cx, + ) + }); + } + } + + cx.notify(); + Ok(()) + } + + // Returns a list of all of the worktrees which no longer have a language server and the root path + // for the stopped server + fn stop_language_server( + &mut self, + worktree_id: WorktreeId, + adapter_name: LanguageServerName, + cx: &mut ModelContext, + ) -> Task<(Option, Vec)> { + let key = (worktree_id, adapter_name); + if let Some(server_id) = self.language_server_ids.remove(&key) { + log::info!("stopping language server {}", key.1 .0); + + // Remove other entries for this language server as well + let mut orphaned_worktrees = vec![worktree_id]; + let other_keys = self.language_server_ids.keys().cloned().collect::>(); + for other_key in other_keys { + if self.language_server_ids.get(&other_key) == Some(&server_id) { + self.language_server_ids.remove(&other_key); + orphaned_worktrees.push(other_key.0); + } + } + + for buffer in self.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade() { + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(server_id, Default::default(), cx); + }); + } + } + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade() { + worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_local_mut() { + worktree.clear_diagnostics_for_language_server(server_id, cx); + } + }); + } + } + + self.language_server_statuses.remove(&server_id); + cx.notify(); + + let server_state = self.language_servers.remove(&server_id); + cx.emit(Event::LanguageServerRemoved(server_id)); + cx.spawn_weak(|this, mut cx| async move { + let mut root_path = None; + + let server = match server_state { + Some(LanguageServerState::Starting(task)) => task.await, + Some(LanguageServerState::Running { server, .. }) => Some(server), + None => None, + }; + + if let Some(server) = server { + root_path = Some(server.root_path().clone()); + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } + } + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.language_server_statuses.remove(&server_id); + cx.notify(); + }); + } + + (root_path, orphaned_worktrees) + }) + } else { + Task::ready((None, Vec::new())) + } + } + + pub fn restart_language_servers_for_buffers( + &mut self, + buffers: impl IntoIterator>, + cx: &mut ModelContext, + ) -> Option<()> { + let language_server_lookup_info: HashSet<(Handle, Arc)> = buffers + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; + let full_path = file.full_path(cx); + let language = self + .languages + .language_for_file(&full_path, Some(buffer.as_rope())) + .now_or_never()? + .ok()?; + Some((file.worktree.clone(), language)) + }) + .collect(); + for (worktree, language) in language_server_lookup_info { + self.restart_language_servers(worktree, language, cx); + } + + None + } + + // TODO This will break in the case where the adapter's root paths and worktrees are not equal + fn restart_language_servers( + &mut self, + worktree: Handle, + language: Arc, + cx: &mut ModelContext, + ) { + let worktree_id = worktree.read(cx).id(); + let fallback_path = worktree.read(cx).abs_path(); + + let mut stops = Vec::new(); + for adapter in language.lsp_adapters() { + stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); + } + + if stops.is_empty() { + return; + } + let mut stops = stops.into_iter(); + + cx.spawn_weak(|this, mut cx| async move { + let (original_root_path, mut orphaned_worktrees) = stops.next().unwrap().await; + for stop in stops { + let (_, worktrees) = stop.await; + orphaned_worktrees.extend_from_slice(&worktrees); + } + + let this = match this.upgrade(&cx) { + Some(this) => this, + None => return, + }; + + this.update(&mut cx, |this, cx| { + // Attempt to restart using original server path. Fallback to passed in + // path if we could not retrieve the root path + let root_path = original_root_path + .map(|path_buf| Arc::from(path_buf.as_path())) + .unwrap_or(fallback_path); + + this.start_language_servers(&worktree, root_path, language.clone(), cx); + + // Lookup new server ids and set them for each of the orphaned worktrees + for adapter in language.lsp_adapters() { + if let Some(new_server_id) = this + .language_server_ids + .get(&(worktree_id, adapter.name.clone())) + .cloned() + { + for &orphaned_worktree in &orphaned_worktrees { + this.language_server_ids + .insert((orphaned_worktree, adapter.name.clone()), new_server_id); + } + } + } + }); + }) + .detach(); + } + + fn check_errored_server( + language: Arc, + adapter: Arc, + server_id: LanguageServerId, + installation_test_binary: Option, + cx: &mut ModelContext, + ) { + if !adapter.can_be_reinstalled() { + log::info!( + "Validation check requested for {:?} but it cannot be reinstalled", + adapter.name.0 + ); + return; + } + + cx.spawn(|this, mut cx| async move { + log::info!("About to spawn test binary"); + + // A lack of test binary counts as a failure + let process = installation_test_binary.and_then(|binary| { + smol::process::Command::new(&binary.path) + .current_dir(&binary.path) + .args(binary.arguments) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .kill_on_drop(true) + .spawn() + .ok() + }); + + const PROCESS_TIMEOUT: Duration = Duration::from_secs(5); + let mut timeout = cx.background().timer(PROCESS_TIMEOUT).fuse(); + + let mut errored = false; + if let Some(mut process) = process { + futures::select! { + status = process.status().fuse() => match status { + Ok(status) => errored = !status.success(), + Err(_) => errored = true, + }, + + _ = timeout => { + log::info!("test binary time-ed out, this counts as a success"); + _ = process.kill(); + } + } + } else { + log::warn!("test binary failed to launch"); + errored = true; + } + + if errored { + log::warn!("test binary check failed"); + let task = this.update(&mut cx, move |this, mut cx| { + this.reinstall_language_server(language, adapter, server_id, &mut cx) + }); + + if let Some(task) = task { + task.await; + } + } + }) + .detach(); + } + + fn on_lsp_progress( + &mut self, + progress: lsp2::ProgressParams, + language_server_id: LanguageServerId, + disk_based_diagnostics_progress_token: Option, + cx: &mut ModelContext, + ) { + let token = match progress.token { + lsp2::NumberOrString::String(token) => token, + lsp2::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); + return; + } + }; + let lsp2::ProgressParamsValue::WorkDone(progress) = progress.value; + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status + } else { + return; + }; + + if !language_server_status.progress_tokens.contains(&token) { + return; + } + + let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token + .as_ref() + .map_or(false, |disk_based_token| { + token.starts_with(disk_based_token) + }); + + match progress { + lsp2::WorkDoneProgress::Begin(report) => { + if is_disk_based_diagnostics_progress { + language_server_status.has_pending_diagnostic_updates = true; + self.disk_based_diagnostics_started(language_server_id, cx); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default()) + }) + .ok(); + } else { + self.on_lsp_work_start( + language_server_id, + token.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::WorkStart( + proto::LspWorkStart { + token, + message: report.message, + percentage: report.percentage.map(|p| p as u32), + }, + ), + }) + .ok(); + } + } + lsp2::WorkDoneProgress::Report(report) => { + if !is_disk_based_diagnostics_progress { + self.on_lsp_work_progress( + language_server_id, + token.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: report.message, + percentage: report.percentage.map(|p| p as u32), + }, + ), + }) + .ok(); + } + } + lsp2::WorkDoneProgress::End(_) => { + language_server_status.progress_tokens.remove(&token); + + if is_disk_based_diagnostics_progress { + language_server_status.has_pending_diagnostic_updates = false; + self.disk_based_diagnostics_finished(language_server_id, cx); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + Default::default(), + ), + }) + .ok(); + } else { + self.on_lsp_work_end(language_server_id, token.clone(), cx); + self.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::WorkEnd( + proto::LspWorkEnd { token }, + ), + }) + .ok(); + } + } + } + } + + fn on_lsp_work_start( + &mut self, + language_server_id: LanguageServerId, + token: String, + progress: LanguageServerProgress, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.insert(token, progress); + cx.notify(); + } + } + + fn on_lsp_work_progress( + &mut self, + language_server_id: LanguageServerId, + token: String, + progress: LanguageServerProgress, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + let entry = status + .pending_work + .entry(token) + .or_insert(LanguageServerProgress { + message: Default::default(), + percentage: Default::default(), + last_update_at: progress.last_update_at, + }); + if progress.message.is_some() { + entry.message = progress.message; + } + if progress.percentage.is_some() { + entry.percentage = progress.percentage; + } + entry.last_update_at = progress.last_update_at; + cx.notify(); + } + } + + fn on_lsp_work_end( + &mut self, + language_server_id: LanguageServerId, + token: String, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + cx.emit(Event::RefreshInlayHints); + status.pending_work.remove(&token); + cx.notify(); + } + } + + fn on_lsp_did_change_watched_files( + &mut self, + language_server_id: LanguageServerId, + params: DidChangeWatchedFilesRegistrationOptions, + cx: &mut ModelContext, + ) { + if let Some(LanguageServerState::Running { watched_paths, .. }) = + self.language_servers.get_mut(&language_server_id) + { + let mut builders = HashMap::default(); + for watcher in params.watchers { + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade() { + let glob_is_inside_worktree = worktree.update(cx, |tree, _| { + if let Some(abs_path) = tree.abs_path().to_str() { + let relative_glob_pattern = match &watcher.glob_pattern { + lsp2::GlobPattern::String(s) => s + .strip_prefix(abs_path) + .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)), + lsp2::GlobPattern::Relative(rp) => { + let base_uri = match &rp.base_uri { + lsp2::OneOf::Left(workspace_folder) => { + &workspace_folder.uri + } + lsp2::OneOf::Right(base_uri) => base_uri, + }; + base_uri.to_file_path().ok().and_then(|file_path| { + (file_path.to_str() == Some(abs_path)) + .then_some(rp.pattern.as_str()) + }) + } + }; + if let Some(relative_glob_pattern) = relative_glob_pattern { + let literal_prefix = + glob_literal_prefix(&relative_glob_pattern); + tree.as_local_mut() + .unwrap() + .add_path_prefix_to_scan(Path::new(literal_prefix).into()); + if let Some(glob) = Glob::new(relative_glob_pattern).log_err() { + builders + .entry(tree.id()) + .or_insert_with(|| GlobSetBuilder::new()) + .add(glob); + } + return true; + } + } + false + }); + if glob_is_inside_worktree { + break; + } + } + } + } + + watched_paths.clear(); + for (worktree_id, builder) in builders { + if let Ok(globset) = builder.build() { + watched_paths.insert(worktree_id, globset); + } + } + + cx.notify(); + } + } + + async fn on_lsp_workspace_edit( + this: WeakHandle, + params: lsp2::ApplyWorkspaceEditParams, + server_id: LanguageServerId, + adapter: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project project closed"))?; + let language_server = this + .read_with(&cx, |this, _| this.language_server_for_id(server_id)) + .ok_or_else(|| anyhow!("language server not found"))?; + let transaction = Self::deserialize_workspace_edit( + this.clone(), + params.edit, + true, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .log_err(); + this.update(&mut cx, |this, _| { + if let Some(transaction) = transaction { + this.last_workspace_edits_by_language_server + .insert(server_id, transaction); + } + }); + Ok(lsp2::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } + + pub fn language_server_statuses( + &self, + ) -> impl DoubleEndedIterator { + self.language_server_statuses.values() + } + + pub fn update_diagnostics( + &mut self, + language_server_id: LanguageServerId, + mut params: lsp2::PublishDiagnosticsParams, + disk_based_sources: &[String], + cx: &mut ModelContext, + ) -> Result<()> { + let abs_path = params + .uri + .to_file_path() + .map_err(|_| anyhow!("URI is not a file"))?; + let mut diagnostics = Vec::default(); + let mut primary_diagnostic_group_ids = HashMap::default(); + let mut sources_by_group_id = HashMap::default(); + let mut supporting_diagnostics = HashMap::default(); + + // Ensure that primary diagnostics are always the most severe + params.diagnostics.sort_by_key(|item| item.severity); + + for diagnostic in ¶ms.diagnostics { + let source = diagnostic.source.as_ref(); + let code = diagnostic.code.as_ref().map(|code| match code { + lsp2::NumberOrString::Number(code) => code.to_string(), + lsp2::NumberOrString::String(code) => code.clone(), + }); + let range = range_from_lsp(diagnostic.range); + let is_supporting = diagnostic + .related_information + .as_ref() + .map_or(false, |infos| { + infos.iter().any(|info| { + primary_diagnostic_group_ids.contains_key(&( + source, + code.clone(), + range_from_lsp(info.location.range), + )) + }) + }); + + let is_unnecessary = diagnostic.tags.as_ref().map_or(false, |tags| { + tags.iter().any(|tag| *tag == DiagnosticTag::UNNECESSARY) + }); + + if is_supporting { + supporting_diagnostics.insert( + (source, code.clone(), range), + (diagnostic.severity, is_unnecessary), + ); + } else { + let group_id = post_inc(&mut self.next_diagnostic_group_id); + let is_disk_based = + source.map_or(false, |source| disk_based_sources.contains(source)); + + sources_by_group_id.insert(group_id, source); + primary_diagnostic_group_ids + .insert((source, code.clone(), range.clone()), group_id); + + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + source: diagnostic.source.clone(), + code: code.clone(), + severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), + message: diagnostic.message.clone(), + group_id, + is_primary: true, + is_valid: true, + is_disk_based, + is_unnecessary, + }, + }); + if let Some(infos) = &diagnostic.related_information { + for info in infos { + if info.location.uri == params.uri && !info.message.is_empty() { + let range = range_from_lsp(info.location.range); + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + source: diagnostic.source.clone(), + code: code.clone(), + severity: DiagnosticSeverity::INFORMATION, + message: info.message.clone(), + group_id, + is_primary: false, + is_valid: true, + is_disk_based, + is_unnecessary: false, + }, + }); + } + } + } + } + } + + for entry in &mut diagnostics { + let diagnostic = &mut entry.diagnostic; + if !diagnostic.is_primary { + let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); + if let Some(&(severity, is_unnecessary)) = supporting_diagnostics.get(&( + source, + diagnostic.code.clone(), + entry.range.clone(), + )) { + if let Some(severity) = severity { + diagnostic.severity = severity; + } + diagnostic.is_unnecessary = is_unnecessary; + } + } + } + + self.update_diagnostic_entries( + language_server_id, + abs_path, + params.version, + diagnostics, + cx, + )?; + Ok(()) + } + + pub fn update_diagnostic_entries( + &mut self, + server_id: LanguageServerId, + abs_path: PathBuf, + version: Option, + diagnostics: Vec>>, + cx: &mut ModelContext, + ) -> Result<(), anyhow::Error> { + let (worktree, relative_path) = self + .find_local_worktree(&abs_path, cx) + .ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?; + + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }; + + if let Some(buffer) = self.get_open_buffer(&project_path, cx) { + self.update_buffer_diagnostics(&buffer, server_id, version, diagnostics.clone(), cx)?; + } + + let updated = worktree.update(cx, |worktree, cx| { + worktree + .as_local_mut() + .ok_or_else(|| anyhow!("not a local worktree"))? + .update_diagnostics(server_id, project_path.path.clone(), diagnostics, cx) + })?; + if updated { + cx.emit(Event::DiagnosticsUpdated { + language_server_id: server_id, + path: project_path, + }); + } + Ok(()) + } + + fn update_buffer_diagnostics( + &mut self, + buffer: &Handle, + server_id: LanguageServerId, + version: Option, + mut diagnostics: Vec>>, + cx: &mut ModelContext, + ) -> Result<()> { + fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { + Ordering::Equal + .then_with(|| b.is_primary.cmp(&a.is_primary)) + .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) + .then_with(|| a.severity.cmp(&b.severity)) + .then_with(|| a.message.cmp(&b.message)) + } + + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?; + + diagnostics.sort_unstable_by(|a, b| { + Ordering::Equal + .then_with(|| a.range.start.cmp(&b.range.start)) + .then_with(|| b.range.end.cmp(&a.range.end)) + .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) + }); + + let mut sanitized_diagnostics = Vec::new(); + let edits_since_save = Patch::new( + snapshot + .edits_since::>(buffer.read(cx).saved_version()) + .collect(), + ); + for entry in diagnostics { + let start; + let end; + if entry.diagnostic.is_disk_based { + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + start = edits_since_save.old_to_new(entry.range.start); + end = edits_since_save.old_to_new(entry.range.end); + } else { + start = entry.range.start; + end = entry.range.end; + } + + let mut range = snapshot.clip_point_utf16(start, Bias::Left) + ..snapshot.clip_point_utf16(end, Bias::Right); + + // Expand empty ranges by one codepoint + if range.start == range.end { + // This will be go to the next boundary when being clipped + range.end.column += 1; + range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); + if range.start == range.end && range.end.column > 0 { + range.start.column -= 1; + range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left); + } + } + + sanitized_diagnostics.push(DiagnosticEntry { + range, + diagnostic: entry.diagnostic, + }); + } + drop(edits_since_save); + + let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(server_id, set, cx) + }); + Ok(()) + } + + pub fn reload_buffers( + &self, + buffers: HashSet>, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + let mut local_buffers = Vec::new(); + let mut remote_buffers = None; + for buffer_handle in buffers { + let buffer = buffer_handle.read(cx); + if buffer.is_dirty() { + if let Some(file) = File::from_dyn(buffer.file()) { + if file.is_local() { + local_buffers.push(buffer_handle); + } else { + remote_buffers.get_or_insert(Vec::new()).push(buffer_handle); + } + } + } + } + + let remote_buffers = self.remote_id().zip(remote_buffers); + let client = self.client.clone(); + + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + + if let Some((project_id, remote_buffers)) = remote_buffers { + let response = client + .request(proto::ReloadBuffers { + project_id, + buffer_ids: remote_buffers + .iter() + .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) + .collect(), + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project_transaction = this + .update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await?; + } + + for buffer in local_buffers { + let transaction = buffer + .update(&mut cx, |buffer, cx| buffer.reload(cx)) + .await?; + buffer.update(&mut cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + project_transaction.0.insert(cx.handle(), transaction); + } + }); + } + + Ok(project_transaction) + }) + } + + pub fn format( + &self, + buffers: HashSet>, + push_to_history: bool, + trigger: FormatTrigger, + cx: &mut ModelContext, + ) -> Task> { + if self.is_local() { + let mut buffers_with_paths_and_servers = buffers + .into_iter() + .filter_map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let file = File::from_dyn(buffer.file())?; + let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); + let server = self + .primary_language_server_for_buffer(buffer, cx) + .map(|s| s.1.clone()); + Some((buffer_handle, buffer_abs_path, server)) + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + // Do not allow multiple concurrent formatting requests for the + // same buffer. + this.update(&mut cx, |this, cx| { + buffers_with_paths_and_servers.retain(|(buffer, _, _)| { + this.buffers_being_formatted + .insert(buffer.read(cx).remote_id()) + }); + }); + + let _cleanup = defer({ + let this = this.clone(); + let mut cx = cx.clone(); + let buffers = &buffers_with_paths_and_servers; + move || { + this.update(&mut cx, |this, cx| { + for (buffer, _, _) in buffers { + this.buffers_being_formatted + .remove(&buffer.read(cx).remote_id()); + } + }); + } + }); + + let mut project_transaction = ProjectTransaction::default(); + for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { + let settings = buffer.read_with(&cx, |buffer, cx| { + language_settings(buffer.language(), buffer.file(), cx).clone() + }); + + let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; + let ensure_final_newline = settings.ensure_final_newline_on_save; + let format_on_save = settings.format_on_save.clone(); + let formatter = settings.formatter.clone(); + let tab_size = settings.tab_size; + + // First, format buffer's whitespace according to the settings. + let trailing_whitespace_diff = if remove_trailing_whitespace { + Some( + buffer + .read_with(&cx, |b, cx| b.remove_trailing_whitespace(cx)) + .await, + ) + } else { + None + }; + let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + if let Some(diff) = trailing_whitespace_diff { + buffer.apply_diff(diff, cx); + } + if ensure_final_newline { + buffer.ensure_final_newline(cx); + } + buffer.end_transaction(cx) + }); + + // Currently, formatting operations are represented differently depending on + // whether they come from a language server or an external command. + enum FormatOperation { + Lsp(Vec<(Range, String)>), + External(Diff), + Prettier(Diff), + } + + // Apply language-specific formatting using either a language server + // or external command. + let mut format_operation = None; + match (formatter, format_on_save) { + (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} + + (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) + | (_, FormatOnSave::LanguageServer) => { + if let Some((language_server, buffer_abs_path)) = + language_server.as_ref().zip(buffer_abs_path.as_ref()) + { + format_operation = Some(FormatOperation::Lsp( + Self::format_via_lsp( + &this, + &buffer, + buffer_abs_path, + &language_server, + tab_size, + &mut cx, + ) + .await + .context("failed to format via language server")?, + )); + } + } + + ( + Formatter::External { command, arguments }, + FormatOnSave::On | FormatOnSave::Off, + ) + | (_, FormatOnSave::External { command, arguments }) => { + if let Some(buffer_abs_path) = buffer_abs_path { + format_operation = Self::format_via_external_command( + buffer, + buffer_abs_path, + &command, + &arguments, + &mut cx, + ) + .await + .context(format!( + "failed to format via external command {:?}", + command + ))? + .map(FormatOperation::External); + } + } + (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during autoformatting: {e:#}" + ), + } + } else if let Some((language_server, buffer_abs_path)) = + language_server.as_ref().zip(buffer_abs_path.as_ref()) + { + format_operation = Some(FormatOperation::Lsp( + Self::format_via_lsp( + &this, + &buffer, + buffer_abs_path, + &language_server, + tab_size, + &mut cx, + ) + .await + .context("failed to format via language server")?, + )); + } + } + (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => { + if let Some(prettier_task) = this + .update(&mut cx, |project, cx| { + project.prettier_instance_for_buffer(buffer, cx) + }).await { + match prettier_task.await + { + Ok(prettier) => { + let buffer_path = buffer.read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).map(|file| file.abs_path(cx)) + }); + format_operation = Some(FormatOperation::Prettier( + prettier + .format(buffer, buffer_path, &cx) + .await + .context("formatting via prettier")?, + )); + } + Err(e) => anyhow::bail!( + "Failed to create prettier instance for buffer during formatting: {e:#}" + ), + } + } + } + }; + + buffer.update(&mut cx, |b, cx| { + // If the buffer had its whitespace formatted and was edited while the language-specific + // formatting was being computed, avoid applying the language-specific formatting, because + // it can't be grouped with the whitespace formatting in the undo history. + if let Some(transaction_id) = whitespace_transaction_id { + if b.peek_undo_stack() + .map_or(true, |e| e.transaction_id() != transaction_id) + { + format_operation.take(); + } + } + + // Apply any language-specific formatting, and group the two formatting operations + // in the buffer's undo history. + if let Some(operation) = format_operation { + match operation { + FormatOperation::Lsp(edits) => { + b.edit(edits, None, cx); + } + FormatOperation::External(diff) => { + b.apply_diff(diff, cx); + } + FormatOperation::Prettier(diff) => { + b.apply_diff(diff, cx); + } + } + + if let Some(transaction_id) = whitespace_transaction_id { + b.group_until_transaction(transaction_id); + } + } + + if let Some(transaction) = b.finalize_last_transaction().cloned() { + if !push_to_history { + b.forget_transaction(transaction.id); + } + project_transaction.0.insert(buffer.clone(), transaction); + } + }); + } + + Ok(project_transaction) + }) + } else { + let remote_id = self.remote_id(); + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + if let Some(project_id) = remote_id { + let response = client + .request(proto::FormatBuffers { + project_id, + trigger: trigger as i32, + buffer_ids: buffers + .iter() + .map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id())) + .collect(), + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project_transaction = this + .update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await?; + } + Ok(project_transaction) + }) + } + } + + async fn format_via_lsp( + this: &Handle, + buffer: &Handle, + abs_path: &Path, + language_server: &Arc, + tab_size: NonZeroU32, + cx: &mut AsyncAppContext, + ) -> Result, String)>> { + let uri = lsp2::Url::from_file_path(abs_path) + .map_err(|_| anyhow!("failed to convert abs path to uri"))?; + let text_document = lsp2::TextDocumentIdentifier::new(uri); + let capabilities = &language_server.capabilities(); + + let formatting_provider = capabilities.document_formatting_provider.as_ref(); + let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); + + let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { + language_server + .request::(lsp2::DocumentFormattingParams { + text_document, + options: lsp_command::lsp_formatting_options(tab_size.get()), + work_done_progress_params: Default::default(), + }) + .await? + } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { + let buffer_start = lsp2::Position::new(0, 0); + let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16())); + + language_server + .request::(lsp2::DocumentRangeFormattingParams { + text_document, + range: lsp2::Range::new(buffer_start, buffer_end), + options: lsp_command::lsp_formatting_options(tab_size.get()), + work_done_progress_params: Default::default(), + }) + .await? + } else { + None + }; + + if let Some(lsp_edits) = lsp_edits { + this.update(cx, |this, cx| { + this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx) + }) + .await + } else { + Ok(Vec::new()) + } + } + + async fn format_via_external_command( + buffer: &Handle, + buffer_abs_path: &Path, + command: &str, + arguments: &[String], + cx: &mut AsyncAppContext, + ) -> Result> { + let working_dir_path = buffer.read_with(cx, |buffer, cx| { + let file = File::from_dyn(buffer.file())?; + let worktree = file.worktree.read(cx).as_local()?; + let mut worktree_path = worktree.abs_path().to_path_buf(); + if worktree.root_entry()?.is_file() { + worktree_path.pop(); + } + Some(worktree_path) + }); + + if let Some(working_dir_path) = working_dir_path { + let mut child = + smol::process::Command::new(command) + .args(arguments.iter().map(|arg| { + arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) + })) + .current_dir(&working_dir_path) + .stdin(smol::process::Stdio::piped()) + .stdout(smol::process::Stdio::piped()) + .stderr(smol::process::Stdio::piped()) + .spawn()?; + let stdin = child + .stdin + .as_mut() + .ok_or_else(|| anyhow!("failed to acquire stdin"))?; + let text = buffer.read_with(cx, |buffer, _| buffer.as_rope().clone()); + for chunk in text.chunks() { + stdin.write_all(chunk.as_bytes()).await?; + } + stdin.flush().await?; + + let output = child.output().await?; + if !output.status.success() { + return Err(anyhow!( + "command failed with exit code {:?}:\nstdout: {}\nstderr: {}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )); + } + + let stdout = String::from_utf8(output.stdout)?; + Ok(Some( + buffer + .read_with(cx, |buffer, cx| buffer.diff(stdout, cx)) + .await, + )) + } else { + Ok(None) + } + } + + pub fn definition( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDefinition { position }, + cx, + ) + } + + pub fn type_definition( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetTypeDefinition { position }, + cx, + ) + } + + pub fn references( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetReferences { position }, + cx, + ) + } + + pub fn document_highlights( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDocumentHighlights { position }, + cx, + ) + } + + pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { + if self.is_local() { + let mut requests = Vec::new(); + for ((worktree_id, _), server_id) in self.language_server_ids.iter() { + let worktree_id = *worktree_id; + let worktree_handle = self.worktree_for_id(worktree_id, cx); + let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) { + Some(worktree) => worktree, + None => continue, + }; + let worktree_abs_path = worktree.abs_path().clone(); + + let (adapter, language, server) = match self.language_servers.get(server_id) { + Some(LanguageServerState::Running { + adapter, + language, + server, + .. + }) => (adapter.clone(), language.clone(), server), + + _ => continue, + }; + + requests.push( + server + .request::( + lsp2::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, + ) + .log_err() + .map(move |response| { + let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { + lsp2::WorkspaceSymbolResponse::Flat(flat_responses) => { + flat_responses.into_iter().map(|lsp_symbol| { + (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) + }).collect::>() + } + lsp2::WorkspaceSymbolResponse::Nested(nested_responses) => { + nested_responses.into_iter().filter_map(|lsp_symbol| { + let location = match lsp_symbol.location { + OneOf::Left(location) => location, + OneOf::Right(_) => { + error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); + return None + } + }; + Some((lsp_symbol.name, lsp_symbol.kind, location)) + }).collect::>() + } + }).unwrap_or_default(); + + ( + adapter, + language, + worktree_id, + worktree_abs_path, + lsp_symbols, + ) + }), + ); + } + + cx.spawn_weak(|this, cx| async move { + let responses = futures::future::join_all(requests).await; + let this = match this.upgrade(&cx) { + Some(this) => this, + None => return Ok(Vec::new()), + }; + + let symbols = this.read_with(&cx, |this, cx| { + let mut symbols = Vec::new(); + for ( + adapter, + adapter_language, + source_worktree_id, + worktree_abs_path, + lsp_symbols, + ) in responses + { + symbols.extend(lsp_symbols.into_iter().filter_map( + |(symbol_name, symbol_kind, symbol_location)| { + let abs_path = symbol_location.uri.to_file_path().ok()?; + let mut worktree_id = source_worktree_id; + let path; + if let Some((worktree, rel_path)) = + this.find_local_worktree(&abs_path, cx) + { + worktree_id = worktree.read(cx).id(); + path = rel_path; + } else { + path = relativize_path(&worktree_abs_path, &abs_path); + } + + let project_path = ProjectPath { + worktree_id, + path: path.into(), + }; + let signature = this.symbol_signature(&project_path); + let adapter_language = adapter_language.clone(); + let language = this + .languages + .language_for_file(&project_path.path, None) + .unwrap_or_else(move |_| adapter_language); + let language_server_name = adapter.name.clone(); + Some(async move { + let language = language.await; + let label = + language.label_for_symbol(&symbol_name, symbol_kind).await; + + Symbol { + language_server_name, + source_worktree_id, + path: project_path, + label: label.unwrap_or_else(|| { + CodeLabel::plain(symbol_name.clone(), None) + }), + kind: symbol_kind, + name: symbol_name, + range: range_from_lsp(symbol_location.range), + signature, + } + }) + }, + )); + } + + symbols + }); + + Ok(futures::future::join_all(symbols).await) + }) + } else if let Some(project_id) = self.remote_id() { + let request = self.client.request(proto::GetProjectSymbols { + project_id, + query: query.to_string(), + }); + cx.spawn_weak(|this, cx| async move { + let response = request.await?; + let mut symbols = Vec::new(); + if let Some(this) = this.upgrade(&cx) { + let new_symbols = this.read_with(&cx, |this, _| { + response + .symbols + .into_iter() + .map(|symbol| this.deserialize_symbol(symbol)) + .collect::>() + }); + symbols = futures::future::join_all(new_symbols) + .await + .into_iter() + .filter_map(|symbol| symbol.log_err()) + .collect::>(); + } + Ok(symbols) + }) + } else { + Task::ready(Ok(Default::default())) + } + } + + pub fn open_buffer_for_symbol( + &mut self, + symbol: &Symbol, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + let language_server_id = if let Some(id) = self.language_server_ids.get(&( + symbol.source_worktree_id, + symbol.language_server_name.clone(), + )) { + *id + } else { + return Task::ready(Err(anyhow!( + "language server for worktree and language not found" + ))); + }; + + let worktree_abs_path = if let Some(worktree_abs_path) = self + .worktree_for_id(symbol.path.worktree_id, cx) + .and_then(|worktree| worktree.read(cx).as_local()) + .map(|local_worktree| local_worktree.abs_path()) + { + worktree_abs_path + } else { + return Task::ready(Err(anyhow!("worktree not found for symbol"))); + }; + let symbol_abs_path = worktree_abs_path.join(&symbol.path.path); + let symbol_uri = if let Ok(uri) = lsp2::Url::from_file_path(symbol_abs_path) { + uri + } else { + return Task::ready(Err(anyhow!("invalid symbol path"))); + }; + + self.open_local_buffer_via_lsp( + symbol_uri, + language_server_id, + symbol.language_server_name.clone(), + cx, + ) + } else if let Some(project_id) = self.remote_id() { + let request = self.client.request(proto::OpenBufferForSymbol { + project_id, + symbol: Some(serialize_symbol(symbol)), + }); + cx.spawn(|this, mut cx| async move { + let response = request.await?; + this.update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(response.buffer_id, cx) + }) + .await + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + pub fn hover( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetHover { position }, + cx, + ) + } + + pub fn completions( + &self, + buffer: &Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let position = position.to_point_utf16(buffer.read(cx)); + if self.is_local() { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(offset); + + let server_ids: Vec<_> = self + .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect(); + + let buffer = buffer.clone(); + cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + GetCompletions { position }, + cx, + )); + } + }); + + let mut completions = Vec::new(); + for task in tasks { + if let Ok(new_completions) = task.await { + completions.extend_from_slice(&new_completions); + } + } + + Ok(completions) + }) + } else if let Some(project_id) = self.remote_id() { + self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx) + } else { + Task::ready(Ok(Default::default())) + } + } + + pub fn apply_additional_edits_for_completion( + &self, + buffer_handle: Handle, + completion: Completion, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + + if self.is_local() { + let server_id = completion.server_id; + let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) { + Some((_, server)) => server.clone(), + _ => return Task::ready(Ok(Default::default())), + }; + + cx.spawn(|this, mut cx| async move { + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + let additional_text_edits = if can_resolve { + lang_server + .request::(completion.lsp2_completion) + .await? + .additional_text_edits + } else { + completion.lsp_completion.additional_text_edits + }; + if let Some(edits) = additional_text_edits { + let edits = this + .update(&mut cx, |this, cx| { + this.edits_from_lsp( + &buffer_handle, + edits, + lang_server.server_id(), + None, + cx, + ) + }) + .await?; + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + + for (range, text) in edits { + let primary = &completion.old_range; + let start_within = primary.start.cmp(&range.start, buffer).is_le() + && primary.end.cmp(&range.start, buffer).is_ge(); + let end_within = range.start.cmp(&primary.end, buffer).is_le() + && range.end.cmp(&primary.end, buffer).is_ge(); + + //Skip additional edits which overlap with the primary completion edit + //https://github.com/zed-industries/zed/pull/1871 + if !start_within && !end_within { + buffer.edit([(range, text)], None, cx); + } + } + + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + Ok(transaction) + }) + } else { + Ok(None) + } + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + cx.spawn(|_, mut cx| async move { + let response = client + .request(proto::ApplyCompletionAdditionalEdits { + project_id, + buffer_id, + completion: Some(language2::proto::serialize_completion(&completion)), + }) + .await?; + + if let Some(transaction) = response.transaction { + let transaction = language2::proto::deserialize_transaction(transaction)?; + buffer_handle + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(transaction.edit_ids.iter().copied()) + }) + .await?; + if push_to_history { + buffer_handle.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction.clone(), Instant::now()); + }); + } + Ok(Some(transaction)) + } else { + Ok(None) + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + pub fn code_actions( + &self, + buffer_handle: &Handle, + range: Range, + cx: &mut ModelContext, + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); + self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + GetCodeActions { range }, + cx, + ) + } + + pub fn apply_code_action( + &self, + buffer_handle: Handle, + mut action: CodeAction, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (lsp_adapter, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, action.server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(Default::default())); + }; + let range = action.range.to_point_utf16(buffer); + + cx.spawn(|this, mut cx| async move { + if let Some(lsp_range) = action + .lsp_action + .data + .as_mut() + .and_then(|d| d.get_mut("codeActionParams")) + .and_then(|d| d.get_mut("range")) + { + *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); + action.lsp2_action = lang_server + .request::(action.lsp2_action) + .await?; + } else { + let actions = this + .update(&mut cx, |this, cx| { + this.code_actions(&buffer_handle, action.range, cx) + }) + .await?; + action.lsp_action = actions + .into_iter() + .find(|a| a.lsp_action.title == action.lsp_action.title) + .ok_or_else(|| anyhow!("code action is outdated"))? + .lsp_action; + } + + if let Some(edit) = action.lsp_action.edit { + if edit.changes.is_some() || edit.document_changes.is_some() { + return Self::deserialize_workspace_edit( + this, + edit, + push_to_history, + lsp_adapter.clone(), + lang_server.clone(), + &mut cx, + ) + .await; + } + } + + if let Some(command) = action.lsp_action.command { + this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + }); + + let result = lang_server + .request::(lsp2::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + ..Default::default() + }) + .await; + + if let Err(err) = result { + // TODO: LSP ERROR + return Err(err); + } + + return Ok(this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + })); + } + + Ok(ProjectTransaction::default()) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ApplyCodeAction { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + action: Some(language2::proto::serialize_code_action(&action)), + }; + cx.spawn(|this, mut cx| async move { + let response = client + .request(request) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + this.update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + }) + .await + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + fn apply_on_type_formatting( + &self, + buffer: Handle, + position: Anchor, + trigger: String, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + cx.spawn(|this, mut cx| async move { + // Do not allow multiple concurrent formatting requests for the + // same buffer. + this.update(&mut cx, |this, cx| { + this.buffers_being_formatted + .insert(buffer.read(cx).remote_id()) + }); + + let _cleanup = defer({ + let this = this.clone(); + let mut cx = cx.clone(); + let closure_buffer = buffer.clone(); + move || { + this.update(&mut cx, |this, cx| { + this.buffers_being_formatted + .remove(&closure_buffer.read(cx).remote_id()); + }); + } + }); + + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(Some(position.timestamp)) + }) + .await?; + this.update(&mut cx, |this, cx| { + let position = position.to_point_utf16(buffer.read(cx)); + this.on_type_format(buffer, position, trigger, false, cx) + }) + .await + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::OnTypeFormatting { + project_id, + buffer_id: buffer.read(cx).remote_id(), + position: Some(serialize_anchor(&position)), + trigger, + version: serialize_version(&buffer.read(cx).version()), + }; + cx.spawn(|_, _| async move { + client + .request(request) + .await? + .transaction + .map(language2::proto::deserialize_transaction) + .transpose() + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + async fn deserialize_edits( + this: Handle, + buffer_to_edit: Handle, + edits: Vec, + push_to_history: bool, + _: Arc, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result> { + let edits = this + .update(cx, |this, cx| { + this.edits_from_lsp( + &buffer_to_edit, + edits, + language_server.server_id(), + None, + cx, + ) + }) + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + + if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + } + }); + + Ok(transaction) + } + + async fn deserialize_workspace_edit( + this: Handle, + edit: lsp2::WorkspaceEdit, + push_to_history: bool, + lsp_adapter: Arc, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let fs = this.read_with(cx, |this, _| this.fs.clone()); + let mut operations = Vec::new(); + if let Some(document_changes) = edit.document_changes { + match document_changes { + lsp2::DocumentChanges::Edits(edits) => { + operations.extend(edits.into_iter().map(lsp2::DocumentChangeOperation::Edit)) + } + lsp2::DocumentChanges::Operations(ops) => operations = ops, + } + } else if let Some(changes) = edit.changes { + operations.extend(changes.into_iter().map(|(uri, edits)| { + lsp2::DocumentChangeOperation::Edit(lsp2::TextDocumentEdit { + text_document: lsp2::OptionalVersionedTextDocumentIdentifier { + uri, + version: None, + }, + edits: edits.into_iter().map(OneOf::Left).collect(), + }) + })); + } + + let mut project_transaction = ProjectTransaction::default(); + for operation in operations { + match operation { + lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Create(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = abs_path.parent() { + fs.create_dir(parent_path).await?; + } + if abs_path.ends_with("/") { + fs.create_dir(&abs_path).await?; + } else { + fs.create_file( + &abs_path, + op.options + .map(|options| fs::CreateOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; + } + } + + lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Rename(op)) => { + let source_abs_path = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target_abs_path = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source_abs_path, + &target_abs_path, + op.options + .map(|options| fs::RenameOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; + } + + lsp2::DocumentChangeOperation::Op(lsp2::ResourceOp::Delete(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op + .options + .map(|options| fs::RemoveOptions { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + }) + .unwrap_or_default(); + if abs_path.ends_with("/") { + fs.remove_dir(&abs_path, options).await?; + } else { + fs.remove_file(&abs_path, options).await?; + } + } + + lsp2::DocumentChangeOperation::Edit(op) => { + let buffer_to_edit = this + .update(cx, |this, cx| { + this.open_local_buffer_via_lsp( + op.text_document.uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) + }) + .await?; + + let edits = this + .update(cx, |this, cx| { + let edits = op.edits.into_iter().map(|edit| match edit { + OneOf::Left(edit) => edit, + OneOf::Right(edit) => edit.text_edit, + }); + this.edits_from_lsp( + &buffer_to_edit, + edits, + language_server.server_id(), + op.text_document.version, + cx, + ) + }) + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + + transaction + }); + if let Some(transaction) = transaction { + project_transaction.0.insert(buffer_to_edit, transaction); + } + } + } + } + + Ok(project_transaction) + } + + pub fn prepare_rename( + &self, + buffer: Handle, + position: T, + cx: &mut ModelContext, + ) -> Task>>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer, + LanguageServerToQuery::Primary, + PrepareRename { position }, + cx, + ) + } + + pub fn perform_rename( + &self, + buffer: Handle, + position: T, + new_name: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer, + LanguageServerToQuery::Primary, + PerformRename { + position, + new_name, + push_to_history, + }, + cx, + ) + } + + pub fn on_type_format( + &self, + buffer: Handle, + position: T, + trigger: String, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task>> { + let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { + let position = position.to_point_utf16(buffer); + ( + position, + language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx) + .tab_size, + ) + }); + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + OnTypeFormatting { + position, + trigger, + options: lsp_command::lsp_formatting_options(tab_size.get()).into(), + push_to_history, + }, + cx, + ) + } + + pub fn inlay_hints( + &self, + buffer_handle: Handle, + range: Range, + cx: &mut ModelContext, + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); + let range_start = range.start; + let range_end = range.end; + let buffer_id = buffer.remote_id(); + let buffer_version = buffer.version().clone(); + let lsp_request = InlayHints { range }; + + if self.is_local() { + let lsp_request_task = self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + lsp_request, + cx, + ); + cx.spawn(|_, mut cx| async move { + buffer_handle + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) + }) + .await + .context("waiting for inlay hint request range edits")?; + lsp_request_task.await.context("inlay hints LSP request") + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::InlayHints { + project_id, + buffer_id, + start: Some(serialize_anchor(&range_start)), + end: Some(serialize_anchor(&range_end)), + version: serialize_version(&buffer_version), + }; + cx.spawn(|project, cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + let hints_request_result = LspCommand::response_from_proto( + lsp_request, + response, + project, + buffer_handle.clone(), + cx, + ) + .await; + + hints_request_result.context("inlay hints proto response conversion") + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + pub fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Handle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Task> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(hint)); + }; + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { + return Task::ready(Ok(hint)); + } + + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|_, mut cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp2_hint(hint, &buffer_snapshot), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; + Ok(resolved_hint) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint.clone())), + }; + cx.spawn(|_, _| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) + .context("inlay hints proto resolve response conversion"), + None => Ok(hint), + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + + #[allow(clippy::type_complexity)] + pub fn search( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(Handle, Vec>)> { + if self.is_local() { + self.search_local(query, cx) + } else if let Some(project_id) = self.remote_id() { + let (tx, rx) = smol::channel::unbounded(); + let request = self.client.request(query.to_proto(project_id)); + cx.spawn(|this, mut cx| async move { + let response = request.await?; + let mut result = HashMap::default(); + for location in response.locations { + let target_buffer = this + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(location.buffer_id, cx) + }) + .await?; + let start = location + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = location + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + result + .entry(target_buffer) + .or_insert(Vec::new()) + .push(start..end) + } + for (buffer, ranges) in result { + let _ = tx.send((buffer, ranges)).await; + } + Result::<(), anyhow::Error>::Ok(()) + }) + .detach_and_log_err(cx); + rx + } else { + unimplemented!(); + } + } + + pub fn search_local( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(Handle, Vec>)> { + // Local search is split into several phases. + // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match + // and the second phase that finds positions of all the matches found in the candidate files. + // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first. + // + // It gets a bit hairy though, because we must account for files that do not have a persistent representation + // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too. + // + // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers). + // Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan + // of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS. + // 2. At this point, we have a list of all potentially matching buffers/files. + // We sort that list by buffer path - this list is retained for later use. + // We ensure that all buffers are now opened and available in project. + // 3. We run a scan over all the candidate buffers on multiple background threads. + // We cannot assume that there will even be a match - while at least one match + // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. + // There is also an auxilliary background thread responsible for result gathering. + // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), + // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. + // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next + // entry - which might already be available thanks to out-of-order processing. + // + // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths. + // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go. + // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index + // in face of constantly updating list of sorted matches. + // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order. + let snapshots = self + .visible_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + let (_, rx) = smol::channel::bounded(1024); + return rx; + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let mut unnamed_files = vec![]; + let opened_buffers = self + .opened_buffers + .iter() + .filter_map(|(_, b)| { + let buffer = b.upgrade()?; + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + if let Some(path) = snapshot.file().map(|file| file.path()) { + Some((path.clone(), (buffer, snapshot))) + } else { + unnamed_files.push(buffer); + None + } + }) + .collect(); + cx.background() + .spawn(Self::background_search( + unnamed_files, + opened_buffers, + cx.background().clone(), + self.fs.clone(), + workers, + query.clone(), + path_count, + snapshots, + matching_paths_tx, + )) + .detach(); + + let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); + let background = cx.background().clone(); + let (result_tx, result_rx) = smol::channel::bounded(1024); + cx.background() + .spawn(async move { + let Ok(buffers) = buffers.await else { + return; + }; + + let buffers_len = buffers.len(); + if buffers_len == 0 { + return; + } + let query = &query; + let (finished_tx, mut finished_rx) = smol::channel::unbounded(); + background + .scoped(|scope| { + #[derive(Clone)] + struct FinishedStatus { + entry: Option<(Handle, Vec>)>, + buffer_index: SearchMatchCandidateIndex, + } + + for _ in 0..workers { + let finished_tx = finished_tx.clone(); + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((entry, buffer_index)) = buffers_rx.next().await { + let buffer_matches = if let Some((_, snapshot)) = entry.as_ref() + { + if query.file_matches( + snapshot.file().map(|file| file.path().as_ref()), + ) { + query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let status = if !buffer_matches.is_empty() { + let entry = if let Some((buffer, _)) = entry.as_ref() { + Some((buffer.clone(), buffer_matches)) + } else { + None + }; + FinishedStatus { + entry, + buffer_index, + } + } else { + FinishedStatus { + entry: None, + buffer_index, + } + }; + if finished_tx.send(status).await.is_err() { + break; + } + } + }); + } + // Report sorted matches + scope.spawn(async move { + let mut current_index = 0; + let mut scratch = vec![None; buffers_len]; + while let Some(status) = finished_rx.next().await { + debug_assert!( + scratch[status.buffer_index].is_none(), + "Got match status of position {} twice", + status.buffer_index + ); + let index = status.buffer_index; + scratch[index] = Some(status); + while current_index < buffers_len { + let Some(current_entry) = scratch[current_index].take() else { + // We intentionally **do not** increment `current_index` here. When next element arrives + // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_) + // this time. + break; + }; + if let Some(entry) = current_entry.entry { + result_tx.send(entry).await.log_err(); + } + current_index += 1; + } + if current_index == buffers_len { + break; + } + } + }); + }) + .await; + }) + .detach(); + result_rx + } + /// Pick paths that might potentially contain a match of a given search query. + async fn background_search( + unnamed_buffers: Vec>, + opened_buffers: HashMap, (Handle, BufferSnapshot)>, + background: Arc, + fs: Arc, + workers: usize, + query: SearchQuery, + path_count: usize, + snapshots: Vec, + matching_paths_tx: Sender, + ) { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let snapshots = &snapshots; + let paths_per_worker = (path_count + workers - 1) / workers; + for buffer in unnamed_buffers { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: None, + }) + .await + .log_err(); + } + for (path, (buffer, _)) in opened_buffers.iter() { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: Some(path.clone()), + }) + .await + .log_err(); + } + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + let unnamed_buffers = opened_buffers.clone(); + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = + worker_start_ix.saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + if unnamed_buffers.contains_key(&entry.path) { + continue; + } + let matches = if query.file_matches(Some(&entry.path)) { + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + if let Some(file) = fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + } + } else { + false + }; + + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }; + if matching_paths_tx.send(project_path).await.is_err() { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; + } + + fn request_lsp( + &self, + buffer_handle: Handle, + server: LanguageServerToQuery, + request: R, + cx: &mut ModelContext, + ) -> Task> + where + ::Result: Send, + { + let buffer = buffer_handle.read(cx); + if self.is_local() { + let language_server = match server { + LanguageServerToQuery::Primary => { + match self.primary_language_server_for_buffer(buffer, cx) { + Some((_, server)) => Some(Arc::clone(server)), + None => return Task::ready(Ok(Default::default())), + } + } + LanguageServerToQuery::Other(id) => self + .language_server_for_buffer(buffer, id, cx) + .map(|(_, server)| Arc::clone(server)), + }; + let file = File::from_dyn(buffer.file()).and_then(File::as_local); + if let (Some(file), Some(language_server)) = (file, language_server) { + let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx); + return cx.spawn(|this, cx| async move { + if !request.check_capabilities(language_server.capabilities()) { + return Ok(Default::default()); + } + + let result = language_server.request::(lsp_params).await; + let response = match result { + Ok(response) => response, + + Err(err) => { + log::warn!( + "Generic lsp request to {} failed: {}", + language_server.name(), + err + ); + return Err(err); + } + }; + + request + .response_from_lsp( + response, + this, + buffer_handle, + language_server.server_id(), + cx, + ) + .await + }); + } + } else if let Some(project_id) = self.remote_id() { + return self.send_lsp_proto_request(buffer_handle, project_id, request, cx); + } + + Task::ready(Ok(Default::default())) + } + + fn send_lsp_proto_request( + &self, + buffer: Handle, + project_id: u64, + request: R, + cx: &mut ModelContext<'_, Project>, + ) -> Task::Response>> { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, buffer.read(cx)); + cx.spawn_weak(|this, cx| async move { + // Ensure the project is still alive by the time the task + // is scheduled. + this.upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + let response = rpc.request(message).await?; + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + if this.read_with(&cx, |this, _| this.is_read_only()) { + Err(anyhow!("disconnected before completing request")) + } else { + request + .response_from_proto(response, this, buffer, cx) + .await + } + }) + } + + fn sort_candidates_and_open_buffers( + mut matching_paths_rx: Receiver, + cx: &mut ModelContext, + ) -> ( + futures::channel::oneshot::Receiver>, + Receiver<( + Option<(Handle, BufferSnapshot)>, + SearchMatchCandidateIndex, + )>, + ) { + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); + cx.spawn(|this, cx| async move { + let mut buffers = vec![]; + while let Some(entry) = matching_paths_rx.next().await { + buffers.push(entry); + } + buffers.sort_by_key(|candidate| candidate.path()); + let matching_paths = buffers.clone(); + let _ = sorted_buffers_tx.send(buffers); + for (index, candidate) in matching_paths.into_iter().enumerate() { + if buffers_tx.is_closed() { + break; + } + let this = this.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + let buffer = match candidate { + SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), + SearchMatchCandidate::Path { worktree_id, path } => this + .update(&mut cx, |this, cx| { + this.open_buffer((worktree_id, path), cx) + }) + .await + .log_err(), + }; + if let Some(buffer) = buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx + .send((Some((buffer, snapshot)), index)) + .await + .log_err(); + } else { + buffers_tx.send((None, index)).await.log_err(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + }) + .detach(); + (sorted_buffers_rx, buffers_rx) + } + + pub fn find_or_create_local_worktree( + &mut self, + abs_path: impl AsRef, + visible: bool, + cx: &mut ModelContext, + ) -> Task, PathBuf)>> { + let abs_path = abs_path.as_ref(); + if let Some((tree, relative_path)) = self.find_local_worktree(abs_path, cx) { + Task::ready(Ok((tree, relative_path))) + } else { + let worktree = self.create_local_worktree(abs_path, visible, cx); + cx.foreground() + .spawn(async move { Ok((worktree.await?, PathBuf::new())) }) + } + } + + pub fn find_local_worktree( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option<(Handle, PathBuf)> { + for tree in &self.worktrees { + if let Some(tree) = tree.upgrade() { + if let Some(relative_path) = tree + .read(cx) + .as_local() + .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) + { + return Some((tree.clone(), relative_path.into())); + } + } + } + None + } + + pub fn is_shared(&self) -> bool { + match &self.client_state { + Some(ProjectClientState::Local { .. }) => true, + _ => false, + } + } + + fn create_local_worktree( + &mut self, + abs_path: impl AsRef, + visible: bool, + cx: &mut ModelContext, + ) -> Task>> { + let fs = self.fs.clone(); + let client = self.client.clone(); + let next_entry_id = self.next_entry_id.clone(); + let path: Arc = abs_path.as_ref().into(); + let task = self + .loading_local_worktrees + .entry(path.clone()) + .or_insert_with(|| { + cx.spawn(|project, mut cx| { + async move { + let worktree = Worktree::local( + client.clone(), + path.clone(), + visible, + fs, + next_entry_id, + &mut cx, + ) + .await; + + project.update(&mut cx, |project, _| { + project.loading_local_worktrees.remove(&path); + }); + + let worktree = worktree?; + project.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx)); + Ok(worktree) + } + .map_err(Arc::new) + }) + .shared() + }) + .clone(); + cx.foreground().spawn(async move { + match task.await { + Ok(worktree) => Ok(worktree), + Err(err) => Err(anyhow!("{}", err)), + } + }) + } + + pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { + self.worktrees.retain(|worktree| { + if let Some(worktree) = worktree.upgrade() { + let id = worktree.read(cx).id(); + if id == id_to_remove { + cx.emit(Event::WorktreeRemoved(id)); + false + } else { + true + } + } else { + false + } + }); + self.metadata_changed(cx); + } + + fn add_worktree(&mut self, worktree: &Handle, cx: &mut ModelContext) { + cx.observe(worktree, |_, _, cx| cx.notify()).detach(); + if worktree.read(cx).is_local() { + cx.subscribe(worktree, |this, worktree, event, cx| match event { + worktree::Event::UpdatedEntries(changes) => { + this.update_local_worktree_buffers(&worktree, changes, cx); + this.update_local_worktree_language_servers(&worktree, changes, cx); + this.update_local_worktree_settings(&worktree, changes, cx); + this.update_prettier_settings(&worktree, changes, cx); + cx.emit(Event::WorktreeUpdatedEntries( + worktree.read(cx).id(), + changes.clone(), + )); + } + worktree::Event::UpdatedGitRepositories(updated_repos) => { + this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) + } + }) + .detach(); + } + + let push_strong_handle = { + let worktree = worktree.read(cx); + self.is_shared() || worktree.is_visible() || worktree.is_remote() + }; + if push_strong_handle { + self.worktrees + .push(WorktreeHandle::Strong(worktree.clone())); + } else { + self.worktrees + .push(WorktreeHandle::Weak(worktree.downgrade())); + } + + let handle_id = worktree.id(); + cx.observe_release(worktree, move |this, worktree, cx| { + let _ = this.remove_worktree(worktree.id(), cx); + cx.update_global::(|store, cx| { + store.clear_local_settings(handle_id, cx).log_err() + }); + }) + .detach(); + + cx.emit(Event::WorktreeAdded); + self.metadata_changed(cx); + } + + fn update_local_worktree_buffers( + &mut self, + worktree_handle: &Handle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext, + ) { + let snapshot = worktree_handle.read(cx).snapshot(); + + let mut renamed_buffers = Vec::new(); + for (path, entry_id, _) in changes { + let worktree_id = worktree_handle.read(cx).id(); + let project_path = ProjectPath { + worktree_id, + path: path.clone(), + }; + + let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) { + Some(&buffer_id) => buffer_id, + None => match self.local_buffer_ids_by_path.get(&project_path) { + Some(&buffer_id) => buffer_id, + None => { + continue; + } + }, + }; + + let open_buffer = self.opened_buffers.get(&buffer_id); + let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade()) { + buffer + } else { + self.opened_buffers.remove(&buffer_id); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_entry_id.remove(entry_id); + continue; + }; + + buffer.update(cx, |buffer, cx| { + if let Some(old_file) = File::from_dyn(buffer.file()) { + if old_file.worktree != *worktree_handle { + return; + } + + let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) { + File { + is_local: true, + entry_id: entry.id, + mtime: entry.mtime, + path: entry.path.clone(), + worktree: worktree_handle.clone(), + is_deleted: false, + } + } else { + File { + is_local: true, + entry_id: old_file.entry_id, + path: old_file.path().clone(), + mtime: old_file.mtime(), + worktree: worktree_handle.clone(), + is_deleted: true, + } + }; + + let old_path = old_file.abs_path(cx); + if new_file.abs_path(cx) != old_path { + renamed_buffers.push((cx.handle(), old_file.clone())); + self.local_buffer_ids_by_path.remove(&project_path); + self.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id, + path: path.clone(), + }, + buffer_id, + ); + } + + if new_file.entry_id != *entry_id { + self.local_buffer_ids_by_entry_id.remove(entry_id); + self.local_buffer_ids_by_entry_id + .insert(new_file.entry_id, buffer_id); + } + + if new_file != *old_file { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateBufferFile { + project_id, + buffer_id: buffer_id as u64, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer.file_updated(Arc::new(new_file), cx).detach(); + } + } + }); + } + + for (buffer, old_file) in renamed_buffers { + self.unregister_buffer_from_language_servers(&buffer, &old_file, cx); + self.detect_language_for_buffer(&buffer, cx); + self.register_buffer_with_language_servers(&buffer, cx); + } + } + + fn update_local_worktree_language_servers( + &mut self, + worktree_handle: &Handle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext, + ) { + if changes.is_empty() { + return; + } + + let worktree_id = worktree_handle.read(cx).id(); + let mut language_server_ids = self + .language_server_ids + .iter() + .filter_map(|((server_worktree_id, _), server_id)| { + (*server_worktree_id == worktree_id).then_some(*server_id) + }) + .collect::>(); + language_server_ids.sort(); + language_server_ids.dedup(); + + let abs_path = worktree_handle.read(cx).abs_path(); + for server_id in &language_server_ids { + if let Some(LanguageServerState::Running { + server, + watched_paths, + .. + }) = self.language_servers.get(server_id) + { + if let Some(watched_paths) = watched_paths.get(&worktree_id) { + let params = lsp2::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, _, change)| { + if !watched_paths.is_match(&path) { + return None; + } + let typ = match change { + PathChange::Loaded => return None, + PathChange::Added => lsp2::FileChangeType::CREATED, + PathChange::Removed => lsp2::FileChangeType::DELETED, + PathChange::Updated => lsp2::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp2::FileChangeType::CHANGED, + }; + Some(lsp2::FileEvent { + uri: lsp2::Url::from_file_path(abs_path.join(path)).unwrap(), + typ, + }) + }) + .collect(), + }; + + if !params.changes.is_empty() { + server + .notify::(params) + .log_err(); + } + } + } + } + } + + fn update_local_worktree_buffers_git_repos( + &mut self, + worktree_handle: Handle, + changed_repos: &UpdatedGitRepositoriesSet, + cx: &mut ModelContext, + ) { + debug_assert!(worktree_handle.read(cx).is_local()); + + // Identify the loading buffers whose containing repository that has changed. + let future_buffers = self + .loading_buffers_by_path + .iter() + .filter_map(|(project_path, receiver)| { + if project_path.worktree_id != worktree_handle.read(cx).id() { + return None; + } + let path = &project_path.path; + changed_repos + .iter() + .find(|(work_dir, _)| path.starts_with(work_dir))?; + let receiver = receiver.clone(); + let path = path.clone(); + Some(async move { + wait_for_loading_buffer(receiver) + .await + .ok() + .map(|buffer| (buffer, path)) + }) + }) + .collect::>(); + + // Identify the current buffers whose containing repository has changed. + let current_buffers = self + .opened_buffers + .values() + .filter_map(|buffer| { + let buffer = buffer.upgrade()?; + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree != worktree_handle { + return None; + } + let path = file.path(); + changed_repos + .iter() + .find(|(work_dir, _)| path.starts_with(work_dir))?; + Some((buffer, path.clone())) + }) + .collect::>(); + + if future_buffers.len() + current_buffers.len() == 0 { + return; + } + + let remote_id = self.remote_id(); + let client = self.client.clone(); + cx.spawn_weak(move |_, mut cx| async move { + // Wait for all of the buffers to load. + let future_buffers = future_buffers.collect::>().await; + + // Reload the diff base for every buffer whose containing git repository has changed. + let snapshot = + worktree_handle.read_with(&cx, |tree, _| tree.as_local().unwrap().snapshot()); + let diff_bases_by_buffer = cx + .background() + .spawn(async move { + future_buffers + .into_iter() + .filter_map(|e| e) + .chain(current_buffers) + .filter_map(|(buffer, path)| { + let (work_directory, repo) = + snapshot.repository_and_work_directory_for_path(&path)?; + let repo = snapshot.get_local_repo(&repo)?; + let relative_path = path.strip_prefix(&work_directory).ok()?; + let base_text = repo.repo_ptr.lock().load_index_text(&relative_path); + Some((buffer, base_text)) + }) + .collect::>() + }) + .await; + + // Assign the new diff bases on all of the buffers. + for (buffer, diff_base) in diff_bases_by_buffer { + let buffer_id = buffer.update(&mut cx, |buffer, cx| { + buffer.set_diff_base(diff_base.clone(), cx); + buffer.remote_id() + }); + if let Some(project_id) = remote_id { + client + .send(proto::UpdateDiffBase { + project_id, + buffer_id, + diff_base, + }) + .log_err(); + } + } + }) + .detach(); + } + + fn update_local_worktree_settings( + &mut self, + worktree: &Handle, + changes: &UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + let project_id = self.remote_id(); + let worktree_id = worktree.id(); + let worktree = worktree.read(cx).as_local().unwrap(); + let remote_worktree_id = worktree.id(); + + let mut settings_contents = Vec::new(); + for (path, _, change) in changes.iter() { + if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let settings_dir = Arc::from( + path.ancestors() + .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) + .unwrap(), + ); + let fs = self.fs.clone(); + let removed = *change == PathChange::Removed; + let abs_path = worktree.absolutize(path); + settings_contents.push(async move { + (settings_dir, (!removed).then_some(fs.load(&abs_path).await)) + }); + } + } + + if settings_contents.is_empty() { + return; + } + + let client = self.client.clone(); + cx.spawn_weak(move |_, mut cx| async move { + let settings_contents: Vec<(Arc, _)> = + futures::future::join_all(settings_contents).await; + cx.update(|cx| { + cx.update_global::(|store, cx| { + for (directory, file_content) in settings_contents { + let file_content = file_content.and_then(|content| content.log_err()); + store + .set_local_settings( + worktree_id, + directory.clone(), + file_content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + if let Some(remote_id) = project_id { + client + .send(proto::UpdateWorktreeSettings { + project_id: remote_id, + worktree_id: remote_worktree_id.to_proto(), + path: directory.to_string_lossy().into_owned(), + content: file_content, + }) + .log_err(); + } + } + }); + }); + }) + .detach(); + } + + fn update_prettier_settings( + &self, + worktree: &Handle, + changes: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut ModelContext<'_, Project>, + ) { + let prettier_config_files = Prettier::CONFIG_FILE_NAMES + .iter() + .map(Path::new) + .collect::>(); + + let prettier_config_file_changed = changes + .iter() + .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) + .filter(|(path, _, _)| { + !path + .components() + .any(|component| component.as_os_str().to_string_lossy() == "node_modules") + }) + .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + let current_worktree_id = worktree.read(cx).id(); + if let Some((config_path, _, _)) = prettier_config_file_changed { + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + let prettiers_to_reload = self + .prettier_instances + .iter() + .filter_map(|((worktree_id, prettier_path), prettier_task)| { + if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) { + Some((*worktree_id, prettier_path.clone(), prettier_task.clone())) + } else { + None + } + }) + .collect::>(); + + cx.background() + .spawn(async move { + for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| { + async move { + prettier_task.await? + .clear_cache() + .await + .with_context(|| { + format!( + "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update" + ) + }) + .map_err(Arc::new) + } + })) + .await + { + if let Err(e) = task_result { + log::error!("Failed to clear cache for prettier: {e:#}"); + } + } + }) + .detach(); + } + } + + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { + let new_active_entry = entry.and_then(|project_path| { + let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_path(project_path.path)?; + Some(entry.id) + }); + if new_active_entry != self.active_entry { + self.active_entry = new_active_entry; + cx.emit(Event::ActiveEntryChanged(new_active_entry)); + } + } + + pub fn language_servers_running_disk_based_diagnostics( + &self, + ) -> impl Iterator + '_ { + self.language_server_statuses + .iter() + .filter_map(|(id, status)| { + if status.has_pending_diagnostic_updates { + Some(*id) + } else { + None + } + }) + } + + pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { + let mut summary = DiagnosticSummary::default(); + for (_, _, path_summary) in self.diagnostic_summaries(cx) { + summary.error_count += path_summary.error_count; + summary.warning_count += path_summary.warning_count; + } + summary + } + + pub fn diagnostic_summaries<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl Iterator + 'a { + self.visible_worktrees(cx).flat_map(move |worktree| { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + worktree + .diagnostic_summaries() + .map(move |(path, server_id, summary)| { + (ProjectPath { worktree_id, path }, server_id, summary) + }) + }) + } + + pub fn disk_based_diagnostics_started( + &mut self, + language_server_id: LanguageServerId, + cx: &mut ModelContext, + ) { + cx.emit(Event::DiskBasedDiagnosticsStarted { language_server_id }); + } + + pub fn disk_based_diagnostics_finished( + &mut self, + language_server_id: LanguageServerId, + cx: &mut ModelContext, + ) { + cx.emit(Event::DiskBasedDiagnosticsFinished { language_server_id }); + } + + pub fn active_entry(&self) -> Option { + self.active_entry + } + + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + self.worktree_for_id(path.worktree_id, cx)? + .read(cx) + .entry_for_path(&path.path) + .cloned() + } + + pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let path = worktree.entry_for_id(entry_id)?.path.clone(); + Some(ProjectPath { worktree_id, path }) + } + + pub fn absolute_path(&self, project_path: &ProjectPath, cx: &AppContext) -> Option { + let workspace_root = self + .worktree_for_id(project_path.worktree_id, cx)? + .read(cx) + .abs_path(); + let project_path = project_path.path.as_ref(); + + Some(if project_path == Path::new("") { + workspace_root.to_path_buf() + } else { + workspace_root.join(project_path) + }) + } + + // RPC message handlers + + async fn handle_unshare_project( + this: Handle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + if this.is_local() { + this.unshare(cx)?; + } else { + this.disconnected_from_host(cx); + } + Ok(()) + }) + } + + async fn handle_add_collaborator( + this: Handle, + mut envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let collaborator = envelope + .payload + .collaborator + .take() + .ok_or_else(|| anyhow!("empty collaborator"))?; + + let collaborator = Collaborator::from_proto(collaborator)?; + this.update(&mut cx, |this, cx| { + this.shared_buffers.remove(&collaborator.peer_id); + cx.emit(Event::CollaboratorJoined(collaborator.peer_id)); + this.collaborators + .insert(collaborator.peer_id, collaborator); + cx.notify(); + }); + + Ok(()) + } + + async fn handle_update_project_collaborator( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let old_peer_id = envelope + .payload + .old_peer_id + .ok_or_else(|| anyhow!("missing old peer id"))?; + let new_peer_id = envelope + .payload + .new_peer_id + .ok_or_else(|| anyhow!("missing new peer id"))?; + this.update(&mut cx, |this, cx| { + let collaborator = this + .collaborators + .remove(&old_peer_id) + .ok_or_else(|| anyhow!("received UpdateProjectCollaborator for unknown peer"))?; + let is_host = collaborator.replica_id == 0; + this.collaborators.insert(new_peer_id, collaborator); + + let buffers = this.shared_buffers.remove(&old_peer_id); + log::info!( + "peer {} became {}. moving buffers {:?}", + old_peer_id, + new_peer_id, + &buffers + ); + if let Some(buffers) = buffers { + this.shared_buffers.insert(new_peer_id, buffers); + } + + if is_host { + this.opened_buffers + .retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_))); + this.buffer_ordered_messages_tx + .unbounded_send(BufferOrderedMessage::Resync) + .unwrap(); + } + + cx.emit(Event::CollaboratorUpdated { + old_peer_id, + new_peer_id, + }); + cx.notify(); + Ok(()) + }) + } + + async fn handle_remove_collaborator( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let peer_id = envelope + .payload + .peer_id + .ok_or_else(|| anyhow!("invalid peer id"))?; + let replica_id = this + .collaborators + .remove(&peer_id) + .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? + .replica_id; + for buffer in this.opened_buffers.values() { + if let Some(buffer) = buffer.upgrade() { + buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); + } + } + this.shared_buffers.remove(&peer_id); + + cx.emit(Event::CollaboratorLeft(peer_id)); + cx.notify(); + Ok(()) + }) + } + + async fn handle_update_project( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + // Don't handle messages that were sent before the response to us joining the project + if envelope.message_id > this.join_project_response_message_id { + this.set_worktrees_from_proto(envelope.payload.worktrees, cx)?; + } + Ok(()) + }) + } + + async fn handle_update_worktree( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + worktree.update(cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + worktree.update_from_remote(envelope.payload); + }); + } + Ok(()) + }) + } + + async fn handle_update_worktree_settings( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + cx.update_global::(|store, cx| { + store + .set_local_settings( + worktree.id(), + PathBuf::from(&envelope.payload.path).into(), + envelope.payload.content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + }); + } + Ok(()) + }) + } + + async fn handle_create_project_entry( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let worktree = this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + this.worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + let path = PathBuf::from(envelope.payload.path); + worktree.create_entry(path, envelope.payload.is_directory, cx) + }) + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_rename_project_entry( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let new_path = PathBuf::from(envelope.payload.new_path); + worktree + .as_local_mut() + .unwrap() + .rename_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_copy_project_entry( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + let entry = worktree + .update(&mut cx, |worktree, cx| { + let new_path = PathBuf::from(envelope.payload.new_path); + worktree + .as_local_mut() + .unwrap() + .copy_entry(entry_id, new_path, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: Some((&entry).into()), + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_delete_project_entry( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + + this.update(&mut cx, |_, cx| cx.emit(Event::DeletedEntry(entry_id))); + + let worktree = this.read_with(&cx, |this, cx| { + this.worktree_for_entry(entry_id, cx) + .ok_or_else(|| anyhow!("worktree not found")) + })?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()); + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .delete_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + Ok(proto::ProjectEntryResponse { + entry: None, + worktree_scan_id: worktree_scan_id as u64, + }) + } + + async fn handle_expand_project_entry( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this + .read_with(&cx, |this, cx| this.worktree_for_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("invalid request"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .expand_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()) as u64; + Ok(proto::ExpandProjectEntryResponse { worktree_scan_id }) + } + + async fn handle_update_diagnostic_summary( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + if let Some(summary) = envelope.payload.summary { + let project_path = ProjectPath { + worktree_id, + path: Path::new(&summary.path).into(), + }; + worktree.update(cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .update_diagnostic_summary(project_path.path.clone(), &summary); + }); + cx.emit(Event::DiagnosticsUpdated { + language_server_id: LanguageServerId(summary.language_server_id as usize), + path: project_path, + }); + } + } + Ok(()) + }) + } + + async fn handle_start_language_server( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let server = envelope + .payload + .server + .ok_or_else(|| anyhow!("invalid server"))?; + this.update(&mut cx, |this, cx| { + this.language_server_statuses.insert( + LanguageServerId(server.id as usize), + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ); + cx.notify(); + }); + Ok(()) + } + + async fn handle_update_language_server( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); + + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid variant"))? + { + proto::update_language_server::Variant::WorkStart(payload) => { + this.on_lsp_work_start( + language_server_id, + payload.token, + LanguageServerProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); + } + + proto::update_language_server::Variant::WorkProgress(payload) => { + this.on_lsp_work_progress( + language_server_id, + payload.token, + LanguageServerProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); + } + + proto::update_language_server::Variant::WorkEnd(payload) => { + this.on_lsp_work_end(language_server_id, payload.token, cx); + } + + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { + this.disk_based_diagnostics_started(language_server_id, cx); + } + + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { + this.disk_based_diagnostics_finished(language_server_id, cx) + } + } + + Ok(()) + }) + } + + async fn handle_update_buffer( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let payload = envelope.payload.clone(); + let buffer_id = payload.buffer_id; + let ops = payload + .operations + .into_iter() + .map(language2::proto::deserialize_operation) + .collect::, _>>()?; + let is_remote = this.is_remote(); + match this.opened_buffers.entry(buffer_id) { + hash_map::Entry::Occupied(mut e) => match e.get_mut() { + OpenBuffer::Strong(buffer) => { + buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; + } + OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops), + OpenBuffer::Weak(_) => {} + }, + hash_map::Entry::Vacant(e) => { + assert!( + is_remote, + "received buffer update from {:?}", + envelope.original_sender_id + ); + e.insert(OpenBuffer::Operations(ops)); + } + } + Ok(proto::Ack {}) + }) + } + + async fn handle_create_buffer_for_peer( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("missing variant"))? + { + proto::create_buffer_for_peer::Variant::State(mut state) => { + let mut buffer_file = None; + if let Some(file) = state.file.take() { + let worktree_id = WorktreeId::from_proto(file.worktree_id); + let worktree = this.worktree_for_id(worktree_id, cx).ok_or_else(|| { + anyhow!("no worktree found for id {}", file.worktree_id) + })?; + buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) + as Arc); + } + + let buffer_id = state.id; + let buffer = cx.add_model(|_| { + Buffer::from_proto(this.replica_id(), state, buffer_file).unwrap() + }); + this.incomplete_remote_buffers + .insert(buffer_id, Some(buffer)); + } + proto::create_buffer_for_peer::Variant::Chunk(chunk) => { + let buffer = this + .incomplete_remote_buffers + .get(&chunk.buffer_id) + .cloned() + .flatten() + .ok_or_else(|| { + anyhow!( + "received chunk for buffer {} without initial state", + chunk.buffer_id + ) + })?; + let operations = chunk + .operations + .into_iter() + .map(language2::proto::deserialize_operation) + .collect::>>()?; + buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + if chunk.is_last { + this.incomplete_remote_buffers.remove(&chunk.buffer_id); + this.register_buffer(&buffer, cx)?; + } + } + } + + Ok(()) + }) + } + + async fn handle_update_diff_base( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let buffer_id = envelope.payload.buffer_id; + let diff_base = envelope.payload.diff_base; + if let Some(buffer) = this + .opened_buffers + .get_mut(&buffer_id) + .and_then(|b| b.upgrade()) + .or_else(|| { + this.incomplete_remote_buffers + .get(&buffer_id) + .cloned() + .flatten() + }) + { + buffer.update(cx, |buffer, cx| buffer.set_diff_base(diff_base, cx)); + } + Ok(()) + }) + } + + async fn handle_update_buffer_file( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let buffer_id = envelope.payload.buffer_id; + + this.update(&mut cx, |this, cx| { + let payload = envelope.payload.clone(); + if let Some(buffer) = this + .opened_buffers + .get(&buffer_id) + .and_then(|b| b.upgrade()) + .or_else(|| { + this.incomplete_remote_buffers + .get(&buffer_id) + .cloned() + .flatten() + }) + { + let file = payload.file.ok_or_else(|| anyhow!("invalid file"))?; + let worktree = this + .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx) + .ok_or_else(|| anyhow!("no such worktree"))?; + let file = File::from_proto(file, worktree, cx)?; + buffer.update(cx, |buffer, cx| { + buffer.file_updated(Arc::new(file), cx).detach(); + }); + this.detect_language_for_buffer(&buffer, cx); + } + Ok(()) + }) + } + + async fn handle_save_buffer( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let buffer_id = envelope.payload.buffer_id; + let (project_id, buffer) = this.update(&mut cx, |this, cx| { + let project_id = this.remote_id().ok_or_else(|| anyhow!("not connected"))?; + let buffer = this + .opened_buffers + .get(&buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; + anyhow::Ok((project_id, buffer)) + })?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&envelope.payload.version)) + }) + .await?; + let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id()); + + this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx)) + .await?; + Ok(buffer.read_with(&cx, |buffer, _| proto::BufferSaved { + project_id, + buffer_id, + version: serialize_version(buffer.saved_version()), + mtime: Some(buffer.saved_mtime().into()), + fingerprint: language2::proto::serialize_fingerprint( + buffer.saved_version_fingerprint(), + ), + })) + } + + async fn handle_reload_buffers( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let reload = this.update(&mut cx, |this, cx| { + let mut buffers = HashSet::default(); + for buffer_id in &envelope.payload.buffer_ids { + buffers.insert( + this.opened_buffers + .get(buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, + ); + } + Ok::<_, anyhow::Error>(this.reload_buffers(buffers, false, cx)) + })?; + + let project_transaction = reload.await?; + let project_transaction = this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) + }); + Ok(proto::ReloadBuffersResponse { + transaction: Some(project_transaction), + }) + } + + async fn handle_synchronize_buffers( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let project_id = envelope.payload.project_id; + let mut response = proto::SynchronizeBuffersResponse { + buffers: Default::default(), + }; + + this.update(&mut cx, |this, cx| { + let Some(guest_id) = envelope.original_sender_id else { + error!("missing original_sender_id on SynchronizeBuffers request"); + return; + }; + + this.shared_buffers.entry(guest_id).or_default().clear(); + for buffer in envelope.payload.buffers { + let buffer_id = buffer.id; + let remote_version = language2::proto::deserialize_version(&buffer.version); + if let Some(buffer) = this.buffer_for_id(buffer_id, cx) { + this.shared_buffers + .entry(guest_id) + .or_default() + .insert(buffer_id); + + let buffer = buffer.read(cx); + response.buffers.push(proto::BufferVersion { + id: buffer_id, + version: language2::proto::serialize_version(&buffer.version), + }); + + let operations = buffer.serialize_ops(Some(remote_version), cx); + let client = this.client.clone(); + if let Some(file) = buffer.file() { + client + .send(proto::UpdateBufferFile { + project_id, + buffer_id: buffer_id as u64, + file: Some(file.to_proto()), + }) + .log_err(); + } + + client + .send(proto::UpdateDiffBase { + project_id, + buffer_id: buffer_id as u64, + diff_base: buffer.diff_base().map(Into::into), + }) + .log_err(); + + client + .send(proto::BufferReloaded { + project_id, + buffer_id, + version: language2::proto::serialize_version(buffer.saved_version()), + mtime: Some(buffer.saved_mtime().into()), + fingerprint: language2::proto::serialize_fingerprint( + buffer.saved_version_fingerprint(), + ), + line_ending: language2::proto::serialize_line_ending( + buffer.line_ending(), + ) as i32, + }) + .log_err(); + + cx.background() + .spawn( + async move { + let operations = operations.await; + for chunk in split_operations(operations) { + client + .request(proto::UpdateBuffer { + project_id, + buffer_id, + operations: chunk, + }) + .await?; + } + anyhow::Ok(()) + } + .log_err(), + ) + .detach(); + } + } + }); + + Ok(response) + } + + async fn handle_format_buffers( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let format = this.update(&mut cx, |this, cx| { + let mut buffers = HashSet::default(); + for buffer_id in &envelope.payload.buffer_ids { + buffers.insert( + this.opened_buffers + .get(buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, + ); + } + let trigger = FormatTrigger::from_proto(envelope.payload.trigger); + Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx)) + })?; + + let project_transaction = format.await?; + let project_transaction = this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) + }); + Ok(proto::FormatBuffersResponse { + transaction: Some(project_transaction), + }) + } + + async fn handle_apply_additional_edits_for_completion( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let (buffer, completion) = this.update(&mut cx, |this, cx| { + let buffer = this + .opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let language = buffer.read(cx).language(); + let completion = language2::proto::deserialize_completion( + envelope + .payload + .completion + .ok_or_else(|| anyhow!("invalid completion"))?, + language.cloned(), + ); + Ok::<_, anyhow::Error>((buffer, completion)) + })?; + + let completion = completion.await?; + + let apply_additional_edits = this.update(&mut cx, |this, cx| { + this.apply_additional_edits_for_completion(buffer, completion, false, cx) + }); + + Ok(proto::ApplyCompletionAdditionalEditsResponse { + transaction: apply_additional_edits + .await? + .as_ref() + .map(language2::proto::serialize_transaction), + }) + } + + async fn handle_apply_code_action( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let action = language2::proto::deserialize_code_action( + envelope + .payload + .action + .ok_or_else(|| anyhow!("invalid action"))?, + )?; + let apply_code_action = this.update(&mut cx, |this, cx| { + let buffer = this + .opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx)) + })?; + + let project_transaction = apply_code_action.await?; + let project_transaction = this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) + }); + Ok(proto::ApplyCodeActionResponse { + transaction: Some(project_transaction), + }) + } + + async fn handle_on_type_formatting( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let on_type_formatting = this.update(&mut cx, |this, cx| { + let buffer = this + .opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let position = envelope + .payload + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + Ok::<_, anyhow::Error>(this.apply_on_type_formatting( + buffer, + position, + envelope.payload.trigger.clone(), + cx, + )) + })?; + + let transaction = on_type_formatting + .await? + .as_ref() + .map(language2::proto::serialize_transaction); + Ok(proto::OnTypeFormattingResponse { transaction }) + } + + async fn handle_inlay_hints( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let buffer_version = deserialize_version(&envelope.payload.version); + + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(buffer_version.clone()) + }) + .await + .with_context(|| { + format!( + "waiting for version {:?} for buffer {}", + buffer_version, + buffer.id() + ) + })?; + + let start = envelope + .payload + .start + .and_then(deserialize_anchor) + .context("missing range start")?; + let end = envelope + .payload + .end + .and_then(deserialize_anchor) + .context("missing range end")?; + let buffer_hints = this + .update(&mut cx, |project, cx| { + project.inlay_hints(buffer, start..end, cx) + }) + .await + .context("inlay hints fetch")?; + + Ok(this.update(&mut cx, |project, cx| { + InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx) + })) + } + + async fn handle_resolve_inlay_hint( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint) + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let response_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")?; + Ok(proto::ResolveInlayHintResponse { + hint: Some(InlayHints::project_to_proto_hint(response_hint)), + }) + } + + async fn handle_refresh_inlay_hints( + this: Handle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |_, cx| { + cx.emit(Event::RefreshInlayHints); + }); + Ok(proto::Ack {}) + } + + async fn handle_lsp_command( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<::Response> + where + ::Result: Send, + { + let sender_id = envelope.original_sender_id()?; + let buffer_id = T::buffer_id_from_proto(&envelope.payload); + let buffer_handle = this.read_with(&cx, |this, _| { + this.opened_buffers + .get(&buffer_id) + .and_then(|buffer| buffer.upgrade(&cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id)) + })?; + let request = T::from_proto( + envelope.payload, + this.clone(), + buffer_handle.clone(), + cx.clone(), + ) + .await?; + let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version()); + let response = this + .update(&mut cx, |this, cx| { + this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + Ok(T::response_to_proto( + response, + this, + sender_id, + &buffer_version, + cx, + )) + }) + } + + async fn handle_get_project_symbols( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let symbols = this + .update(&mut cx, |this, cx| { + this.symbols(&envelope.payload.query, cx) + }) + .await?; + + Ok(proto::GetProjectSymbolsResponse { + symbols: symbols.iter().map(serialize_symbol).collect(), + }) + } + + async fn handle_search_project( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let query = SearchQuery::from_proto(envelope.payload)?; + let mut result = this.update(&mut cx, |this, cx| this.search(query, cx)); + + cx.spawn(|mut cx| async move { + let mut locations = Vec::new(); + while let Some((buffer, ranges)) = result.next().await { + for range in ranges { + let start = serialize_anchor(&range.start); + let end = serialize_anchor(&range.end); + let buffer_id = this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }); + locations.push(proto::Location { + buffer_id, + start: Some(start), + end: Some(end), + }); + } + } + Ok(proto::SearchProjectResponse { locations }) + }) + .await + } + + async fn handle_open_buffer_for_symbol( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let symbol = envelope + .payload + .symbol + .ok_or_else(|| anyhow!("invalid symbol"))?; + let symbol = this + .read_with(&cx, |this, _| this.deserialize_symbol(symbol)) + .await?; + let symbol = this.read_with(&cx, |this, _| { + let signature = this.symbol_signature(&symbol.path); + if signature == symbol.signature { + Ok(symbol) + } else { + Err(anyhow!("invalid symbol signature")) + } + })?; + let buffer = this + .update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx)) + .await?; + + Ok(proto::OpenBufferForSymbolResponse { + buffer_id: this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }), + }) + } + + fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(project_path.worktree_id.to_proto().to_be_bytes()); + hasher.update(project_path.path.to_string_lossy().as_bytes()); + hasher.update(self.nonce.to_be_bytes()); + hasher.finalize().as_slice().try_into().unwrap() + } + + async fn handle_open_buffer_by_id( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let buffer = this + .update(&mut cx, |this, cx| { + this.open_buffer_by_id(envelope.payload.id, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + Ok(proto::OpenBufferResponse { + buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx), + }) + }) + } + + async fn handle_open_buffer_by_path( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let open_buffer = this.update(&mut cx, |this, cx| { + this.open_buffer( + ProjectPath { + worktree_id, + path: PathBuf::from(envelope.payload.path).into(), + }, + cx, + ) + }); + + let buffer = open_buffer.await?; + this.update(&mut cx, |this, cx| { + Ok(proto::OpenBufferResponse { + buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx), + }) + }) + } + + fn serialize_project_transaction_for_peer( + &mut self, + project_transaction: ProjectTransaction, + peer_id: proto::PeerId, + cx: &mut AppContext, + ) -> proto::ProjectTransaction { + let mut serialized_transaction = proto::ProjectTransaction { + buffer_ids: Default::default(), + transactions: Default::default(), + }; + for (buffer, transaction) in project_transaction.0 { + serialized_transaction + .buffer_ids + .push(self.create_buffer_for_peer(&buffer, peer_id, cx)); + serialized_transaction + .transactions + .push(language2::proto::serialize_transaction(&transaction)); + } + serialized_transaction + } + + fn deserialize_project_transaction( + &mut self, + message: proto::ProjectTransaction, + push_to_history: bool, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(|this, mut cx| async move { + let mut project_transaction = ProjectTransaction::default(); + for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions) + { + let buffer = this + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(buffer_id, cx) + }) + .await?; + let transaction = language2::proto::deserialize_transaction(transaction)?; + project_transaction.0.insert(buffer, transaction); + } + + for (buffer, transaction) in &project_transaction.0 { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(transaction.edit_ids.iter().copied()) + }) + .await?; + + if push_to_history { + buffer.update(&mut cx, |buffer, _| { + buffer.push_transaction(transaction.clone(), Instant::now()); + }); + } + } + + Ok(project_transaction) + }) + } + + fn create_buffer_for_peer( + &mut self, + buffer: &Handle, + peer_id: proto::PeerId, + cx: &mut AppContext, + ) -> u64 { + let buffer_id = buffer.read(cx).remote_id(); + if let Some(ProjectClientState::Local { updates_tx, .. }) = &self.client_state { + updates_tx + .unbounded_send(LocalProjectUpdate::CreateBufferForPeer { peer_id, buffer_id }) + .ok(); + } + buffer_id + } + + fn wait_for_remote_buffer( + &mut self, + id: u64, + cx: &mut ModelContext, + ) -> Task>> { + let mut opened_buffer_rx = self.opened_buffer.1.clone(); + + cx.spawn_weak(|this, mut cx| async move { + let buffer = loop { + let Some(this) = this.upgrade(&cx) else { + return Err(anyhow!("project dropped")); + }; + + let buffer = this.read_with(&cx, |this, cx| { + this.opened_buffers + .get(&id) + .and_then(|buffer| buffer.upgrade()) + }); + + if let Some(buffer) = buffer { + break buffer; + } else if this.read_with(&cx, |this, _| this.is_read_only()) { + return Err(anyhow!("disconnected before buffer {} could be opened", id)); + } + + this.update(&mut cx, |this, _| { + this.incomplete_remote_buffers.entry(id).or_default(); + }); + drop(this); + + opened_buffer_rx + .next() + .await + .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?; + }; + + Ok(buffer) + }) + } + + fn synchronize_remote_buffers(&mut self, cx: &mut ModelContext) -> Task> { + let project_id = match self.client_state.as_ref() { + Some(ProjectClientState::Remote { + sharing_has_stopped, + remote_id, + .. + }) => { + if *sharing_has_stopped { + return Task::ready(Err(anyhow!( + "can't synchronize remote buffers on a readonly project" + ))); + } else { + *remote_id + } + } + Some(ProjectClientState::Local { .. }) | None => { + return Task::ready(Err(anyhow!( + "can't synchronize remote buffers on a local project" + ))) + } + }; + + let client = self.client.clone(); + cx.spawn(|this, cx| async move { + let (buffers, incomplete_buffer_ids) = this.read_with(&cx, |this, cx| { + let buffers = this + .opened_buffers + .iter() + .filter_map(|(id, buffer)| { + let buffer = buffer.upgrade()?; + Some(proto::BufferVersion { + id: *id, + version: language2::proto::serialize_version(&buffer.read(cx).version), + }) + }) + .collect(); + let incomplete_buffer_ids = this + .incomplete_remote_buffers + .keys() + .copied() + .collect::>(); + + (buffers, incomplete_buffer_ids) + }); + let response = client + .request(proto::SynchronizeBuffers { + project_id, + buffers, + }) + .await?; + + let send_updates_for_buffers = response.buffers.into_iter().map(|buffer| { + let client = client.clone(); + let buffer_id = buffer.id; + let remote_version = language2::proto::deserialize_version(&buffer.version); + this.read_with(&cx, |this, cx| { + if let Some(buffer) = this.buffer_for_id(buffer_id, cx) { + let operations = buffer.read(cx).serialize_ops(Some(remote_version), cx); + cx.background().spawn(async move { + let operations = operations.await; + for chunk in split_operations(operations) { + client + .request(proto::UpdateBuffer { + project_id, + buffer_id, + operations: chunk, + }) + .await?; + } + anyhow::Ok(()) + }) + } else { + Task::ready(Ok(())) + } + }) + }); + + // Any incomplete buffers have open requests waiting. Request that the host sends + // creates these buffers for us again to unblock any waiting futures. + for id in incomplete_buffer_ids { + cx.background() + .spawn(client.request(proto::OpenBufferById { project_id, id })) + .detach(); + } + + futures::future::join_all(send_updates_for_buffers) + .await + .into_iter() + .collect() + }) + } + + pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec { + self.worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + proto::WorktreeMetadata { + id: worktree.id().to_proto(), + root_name: worktree.root_name().into(), + visible: worktree.is_visible(), + abs_path: worktree.abs_path().to_string_lossy().into(), + } + }) + .collect() + } + + fn set_worktrees_from_proto( + &mut self, + worktrees: Vec, + cx: &mut ModelContext, + ) -> Result<()> { + let replica_id = self.replica_id(); + let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?; + + let mut old_worktrees_by_id = self + .worktrees + .drain(..) + .filter_map(|worktree| { + let worktree = worktree.upgrade()?; + Some((worktree.read(cx).id(), worktree)) + }) + .collect::>(); + + for worktree in worktrees { + if let Some(old_worktree) = + old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id)) + { + self.worktrees.push(WorktreeHandle::Strong(old_worktree)); + } else { + let worktree = + Worktree::remote(remote_id, replica_id, worktree, self.client.clone(), cx); + let _ = self.add_worktree(&worktree, cx); + } + } + + self.metadata_changed(cx); + for id in old_worktrees_by_id.keys() { + cx.emit(Event::WorktreeRemoved(*id)); + } + + Ok(()) + } + + fn set_collaborators_from_proto( + &mut self, + messages: Vec, + cx: &mut ModelContext, + ) -> Result<()> { + let mut collaborators = HashMap::default(); + for message in messages { + let collaborator = Collaborator::from_proto(message)?; + collaborators.insert(collaborator.peer_id, collaborator); + } + for old_peer_id in self.collaborators.keys() { + if !collaborators.contains_key(old_peer_id) { + cx.emit(Event::CollaboratorLeft(*old_peer_id)); + } + } + self.collaborators = collaborators; + Ok(()) + } + + fn deserialize_symbol( + &self, + serialized_symbol: proto::Symbol, + ) -> impl Future> { + let languages = self.languages.clone(); + async move { + let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); + let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); + let start = serialized_symbol + .start + .ok_or_else(|| anyhow!("invalid start"))?; + let end = serialized_symbol + .end + .ok_or_else(|| anyhow!("invalid end"))?; + let kind = unsafe { mem::transmute(serialized_symbol.kind) }; + let path = ProjectPath { + worktree_id, + path: PathBuf::from(serialized_symbol.path).into(), + }; + let language = languages + .language_for_file(&path.path, None) + .await + .log_err(); + Ok(Symbol { + language_server_name: LanguageServerName( + serialized_symbol.language_server_name.into(), + ), + source_worktree_id, + path, + label: { + match language { + Some(language) => { + language + .label_for_symbol(&serialized_symbol.name, kind) + .await + } + None => None, + } + .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)) + }, + + name: serialized_symbol.name, + range: Unclipped(PointUtf16::new(start.row, start.column)) + ..Unclipped(PointUtf16::new(end.row, end.column)), + kind, + signature: serialized_symbol + .signature + .try_into() + .map_err(|_| anyhow!("invalid signature"))?, + }) + } + } + + async fn handle_buffer_saved( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let fingerprint = deserialize_fingerprint(&envelope.payload.fingerprint)?; + let version = deserialize_version(&envelope.payload.version); + let mtime = envelope + .payload + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + + this.update(&mut cx, |this, cx| { + let buffer = this + .opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .or_else(|| { + this.incomplete_remote_buffers + .get(&envelope.payload.buffer_id) + .and_then(|b| b.clone()) + }); + if let Some(buffer) = buffer { + buffer.update(cx, |buffer, cx| { + buffer.did_save(version, fingerprint, mtime, cx); + }); + } + Ok(()) + }) + } + + async fn handle_buffer_reloaded( + this: Handle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let payload = envelope.payload; + let version = deserialize_version(&payload.version); + let fingerprint = deserialize_fingerprint(&payload.fingerprint)?; + let line_ending = deserialize_line_ending( + proto::LineEnding::from_i32(payload.line_ending) + .ok_or_else(|| anyhow!("missing line ending"))?, + ); + let mtime = payload + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + this.update(&mut cx, |this, cx| { + let buffer = this + .opened_buffers + .get(&payload.buffer_id) + .and_then(|buffer| buffer.upgrade()) + .or_else(|| { + this.incomplete_remote_buffers + .get(&payload.buffer_id) + .cloned() + .flatten() + }); + if let Some(buffer) = buffer { + buffer.update(cx, |buffer, cx| { + buffer.did_reload(version, fingerprint, line_ending, mtime, cx); + }); + } + Ok(()) + }) + } + + #[allow(clippy::type_complexity)] + fn edits_from_lsp( + &mut self, + buffer: &Handle, + lsp2_edits: impl 'static + Send + IntoIterator, + server_id: LanguageServerId, + version: Option, + cx: &mut ModelContext, + ) -> Task, String)>>> { + let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); + cx.background().spawn(async move { + let snapshot = snapshot?; + let mut lsp_edits = lsp_edits + .into_iter() + .map(|edit| (range_from_lsp(edit.range), edit.new_text)) + .collect::>(); + lsp_edits.sort_by_key(|(range, _)| range.start); + + let mut lsp_edits = lsp_edits.into_iter().peekable(); + let mut edits = Vec::new(); + while let Some((range, mut new_text)) = lsp_edits.next() { + // Clip invalid ranges provided by the language server. + let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) + ..snapshot.clip_point_utf16(range.end, Bias::Left); + + // Combine any LSP edits that are adjacent. + // + // Also, combine LSP edits that are separated from each other by only + // a newline. This is important because for some code actions, + // Rust-analyzer rewrites the entire buffer via a series of edits that + // are separated by unchanged newline characters. + // + // In order for the diffing logic below to work properly, any edits that + // cancel each other out must be combined into one. + while let Some((next_range, next_text)) = lsp_edits.peek() { + if next_range.start.0 > range.end { + if next_range.start.0.row > range.end.row + 1 + || next_range.start.0.column > 0 + || snapshot.clip_point_utf16( + Unclipped(PointUtf16::new(range.end.row, u32::MAX)), + Bias::Left, + ) > range.end + { + break; + } + new_text.push('\n'); + } + range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); + new_text.push_str(next_text); + lsp_edits.next(); + } + + // For multiline edits, perform a diff of the old and new text so that + // we can identify the changes more precisely, preserving the locations + // of any anchors positioned in the unchanged regions. + if range.end.row > range.start.row { + let mut offset = range.start.to_offset(&snapshot); + let old_text = snapshot.text_for_range(range).collect::(); + + let diff = TextDiff::from_lines(old_text.as_str(), &new_text); + let mut moved_since_edit = true; + for change in diff.iter_all_changes() { + let tag = change.tag(); + let value = change.value(); + match tag { + ChangeTag::Equal => { + offset += value.len(); + moved_since_edit = true; + } + ChangeTag::Delete => { + let start = snapshot.anchor_after(offset); + let end = snapshot.anchor_before(offset + value.len()); + if moved_since_edit { + edits.push((start..end, String::new())); + } else { + edits.last_mut().unwrap().0.end = end; + } + offset += value.len(); + moved_since_edit = false; + } + ChangeTag::Insert => { + if moved_since_edit { + let anchor = snapshot.anchor_after(offset); + edits.push((anchor..anchor, value.to_string())); + } else { + edits.last_mut().unwrap().1.push_str(value); + } + moved_since_edit = false; + } + } + } + } else if range.end == range.start { + let anchor = snapshot.anchor_after(range.start); + edits.push((anchor..anchor, new_text)); + } else { + let edit_start = snapshot.anchor_after(range.start); + let edit_end = snapshot.anchor_before(range.end); + edits.push((edit_start..edit_end, new_text)); + } + } + + Ok(edits) + }) + } + + fn buffer_snapshot_for_lsp_version( + &mut self, + buffer: &Handle, + server_id: LanguageServerId, + version: Option, + cx: &AppContext, + ) -> Result { + const OLD_VERSIONS_TO_RETAIN: i32 = 10; + + if let Some(version) = version { + let buffer_id = buffer.read(cx).remote_id(); + let snapshots = self + .buffer_snapshots + .get_mut(&buffer_id) + .and_then(|m| m.get_mut(&server_id)) + .ok_or_else(|| { + anyhow!("no snapshots found for buffer {buffer_id} and server {server_id}") + })?; + + let found_snapshot = snapshots + .binary_search_by_key(&version, |e| e.version) + .map(|ix| snapshots[ix].snapshot.clone()) + .map_err(|_| { + anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}") + })?; + + snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version); + Ok(found_snapshot) + } else { + Ok((buffer.read(cx)).text_snapshot()) + } + } + + pub fn language_servers( + &self, + ) -> impl '_ + Iterator { + self.language_server_ids + .iter() + .map(|((worktree_id, server_name), server_id)| { + (*server_id, server_name.clone(), *worktree_id) + }) + } + + pub fn supplementary_language_servers( + &self, + ) -> impl '_ + + Iterator< + Item = ( + &LanguageServerId, + &(LanguageServerName, Arc), + ), + > { + self.supplementary_language_servers.iter() + } + + pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { + if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { + Some(server.clone()) + } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) { + Some(Arc::clone(server)) + } else { + None + } + } + + pub fn language_servers_for_buffer( + &self, + buffer: &Buffer, + cx: &AppContext, + ) -> impl Iterator, &Arc)> { + self.language_server_ids_for_buffer(buffer, cx) + .into_iter() + .filter_map(|server_id| match self.language_servers.get(&server_id)? { + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter, server)), + _ => None, + }) + } + + fn primary_language_server_for_buffer( + &self, + buffer: &Buffer, + cx: &AppContext, + ) -> Option<(&Arc, &Arc)> { + self.language_servers_for_buffer(buffer, cx).next() + } + + pub fn language_server_for_buffer( + &self, + buffer: &Buffer, + server_id: LanguageServerId, + cx: &AppContext, + ) -> Option<(&Arc, &Arc)> { + self.language_servers_for_buffer(buffer, cx) + .find(|(_, s)| s.server_id() == server_id) + } + + fn language_server_ids_for_buffer( + &self, + buffer: &Buffer, + cx: &AppContext, + ) -> Vec { + if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { + let worktree_id = file.worktree_id(cx); + language + .lsp_adapters() + .iter() + .flat_map(|adapter| { + let key = (worktree_id, adapter.name.clone()); + self.language_server_ids.get(&key).copied() + }) + .collect() + } else { + Vec::new() + } + } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &Handle, + cx: &mut ModelContext, + ) -> Task, Arc>>>>> { + let buffer = buffer.read(cx); + let buffer_file = buffer.file(); + let Some(buffer_language) = buffer.language() else { + return Task::ready(None); + }; + if !buffer_language + .lsp_adapters() + .iter() + .flat_map(|adapter| adapter.enabled_formatters()) + .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. })) + { + return Task::ready(None); + } + + let buffer_file = File::from_dyn(buffer_file); + let buffer_path = buffer_file.map(|file| Arc::clone(file.path())); + let worktree_path = buffer_file + .as_ref() + .and_then(|file| Some(file.worktree.read(cx).abs_path())); + let worktree_id = buffer_file.map(|file| file.worktree_id(cx)); + if self.is_local() || worktree_id.is_none() || worktree_path.is_none() { + let Some(node) = self.node.as_ref().map(Arc::clone) else { + return Task::ready(None); + }; + cx.spawn(|this, mut cx| async move { + let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs)); + let prettier_dir = match cx + .background() + .spawn(Prettier::locate( + worktree_path.zip(buffer_path).map( + |(worktree_root_path, starting_path)| LocateStart { + worktree_root_path, + starting_path, + }, + ), + fs, + )) + .await + { + Ok(path) => path, + Err(e) => { + return Some( + Task::ready(Err(Arc::new(e.context( + "determining prettier path for worktree {worktree_path:?}", + )))) + .shared(), + ); + } + }; + + if let Some(existing_prettier) = this.update(&mut cx, |project, _| { + project + .prettier_instances + .get(&(worktree_id, prettier_dir.clone())) + .cloned() + }) { + return Some(existing_prettier); + } + + log::info!("Found prettier in {prettier_dir:?}, starting."); + let task_prettier_dir = prettier_dir.clone(); + let weak_project = this.downgrade(); + let new_server_id = + this.update(&mut cx, |this, _| this.languages.next_language_server_id()); + let new_prettier_task = cx + .spawn(|mut cx| async move { + let prettier = Prettier::start( + worktree_id.map(|id| id.to_usize()), + new_server_id, + task_prettier_dir, + node, + cx.clone(), + ) + .await + .context("prettier start") + .map_err(Arc::new)?; + log::info!("Started prettier in {:?}", prettier.prettier_dir()); + + if let Some((project, prettier_server)) = + weak_project.upgrade(&mut cx).zip(prettier.server()) + { + project.update(&mut cx, |project, cx| { + let name = if prettier.is_default() { + LanguageServerName(Arc::from("prettier (default)")) + } else { + let prettier_dir = prettier.prettier_dir(); + let worktree_path = prettier + .worktree_id() + .map(WorktreeId::from_usize) + .and_then(|id| project.worktree_for_id(id, cx)) + .map(|worktree| worktree.read(cx).abs_path()); + match worktree_path { + Some(worktree_path) => { + if worktree_path.as_ref() == prettier_dir { + LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default() + ))) + } else { + let dir_to_display = match prettier_dir + .strip_prefix(&worktree_path) + .ok() + { + Some(relative_path) => relative_path, + None => prettier_dir, + }; + LanguageServerName(Arc::from(format!( + "prettier ({})", + dir_to_display.display(), + ))) + } + } + None => LanguageServerName(Arc::from(format!( + "prettier ({})", + prettier_dir.display(), + ))), + } + }; + + project + .supplementary_language_servers + .insert(new_server_id, (name, Arc::clone(prettier_server))); + cx.emit(Event::LanguageServerAdded(new_server_id)); + }); + } + Ok(Arc::new(prettier)).map_err(Arc::new) + }) + .shared(); + this.update(&mut cx, |project, _| { + project + .prettier_instances + .insert((worktree_id, prettier_dir), new_prettier_task.clone()); + }); + Some(new_prettier_task) + }) + } else if self.remote_id().is_some() { + return Task::ready(None); + } else { + Task::ready(Some( + Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(), + )) + } + } + + fn install_default_formatters( + &self, + worktree: Option, + new_language: &Language, + language_settings: &LanguageSettings, + cx: &mut ModelContext, + ) -> Task> { + match &language_settings.formatter { + Formatter::Prettier { .. } | Formatter::Auto => {} + Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())), + }; + let Some(node) = self.node.as_ref().cloned() else { + return Task::ready(Ok(())); + }; + + let mut prettier_plugins = None; + for formatter in new_language + .lsp_adapters() + .into_iter() + .flat_map(|adapter| adapter.enabled_formatters()) + { + match formatter { + BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins + .get_or_insert_with(|| HashSet::default()) + .extend(plugin_names), + } + } + let Some(prettier_plugins) = prettier_plugins else { + return Task::ready(Ok(())); + }; + + let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path(); + let already_running_prettier = self + .prettier_instances + .get(&(worktree, default_prettier_dir.to_path_buf())) + .cloned(); + + let fs = Arc::clone(&self.fs); + cx.background() + .spawn(async move { + let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); + // method creates parent directory if it doesn't exist + fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await + .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; + + let packages_to_versions = future::try_join_all( + prettier_plugins + .iter() + .chain(Some(&"prettier")) + .map(|package_name| async { + let returned_package_name = package_name.to_string(); + let latest_version = node.npm_package_latest_version(package_name) + .await + .with_context(|| { + format!("fetching latest npm version for package {returned_package_name}") + })?; + anyhow::Ok((returned_package_name, latest_version)) + }), + ) + .await + .context("fetching latest npm versions")?; + + log::info!("Fetching default prettier and plugins: {packages_to_versions:?}"); + let borrowed_packages = packages_to_versions.iter().map(|(package, version)| { + (package.as_str(), version.as_str()) + }).collect::>(); + node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?; + + if !prettier_plugins.is_empty() { + if let Some(prettier) = already_running_prettier { + prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?; + } + } + + anyhow::Ok(()) + }) + } +} + +fn subscribe_for_copilot_events( + copilot: &Handle, + cx: &mut ModelContext<'_, Project>, +) -> gpui2::Subscription { + cx.subscribe( + copilot, + |project, copilot, copilot_event, cx| match copilot_event { + copilot::Event::CopilotLanguageServerStarted => { + match copilot.read(cx).language_server() { + Some((name, copilot_server)) => { + // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. + if !copilot_server.has_notification_handler::() { + let new_server_id = copilot_server.server_id(); + let weak_project = cx.weak_handle(); + let copilot_log_subscription = copilot_server + .on_notification::( + move |params, mut cx| { + if let Some(project) = weak_project.upgrade(&mut cx) { + project.update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerLog( + new_server_id, + params.message, + )); + }) + } + }, + ); + project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server))); + project.copilot_log_subscription = Some(copilot_log_subscription); + cx.emit(Event::LanguageServerAdded(new_server_id)); + } + } + None => debug_panic!("Received Copilot language server started event, but no language server is running"), + } + } + }, + ) +} + +fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str { + let mut literal_end = 0; + for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() { + if part.contains(&['*', '?', '{', '}']) { + break; + } else { + if i > 0 { + // Acount for separator prior to this part + literal_end += path::MAIN_SEPARATOR.len_utf8(); + } + literal_end += part.len(); + } + } + &glob[..literal_end] +} + +impl WorktreeHandle { + pub fn upgrade(&self) -> Option> { + match self { + WorktreeHandle::Strong(handle) => Some(handle.clone()), + WorktreeHandle::Weak(handle) => handle.upgrade(), + } + } + + pub fn handle_id(&self) -> usize { + match self { + WorktreeHandle::Strong(handle) => handle.id(), + WorktreeHandle::Weak(handle) => handle.id(), + } + } +} + +impl OpenBuffer { + pub fn upgrade(&self) -> Option> { + match self { + OpenBuffer::Strong(handle) => Some(handle.clone()), + OpenBuffer::Weak(handle) => handle.upgrade(), + OpenBuffer::Operations(_) => None, + } + } +} + +pub struct PathMatchCandidateSet { + pub snapshot: Snapshot, + pub include_ignored: bool, + pub include_root_name: bool, +} + +impl<'a> fuzzy2::PathMatchCandidateSet<'a> for PathMatchCandidateSet { + type Candidates = PathMatchCandidateSetIter<'a>; + + fn id(&self) -> usize { + self.snapshot.id().to_usize() + } + + fn len(&self) -> usize { + if self.include_ignored { + self.snapshot.file_count() + } else { + self.snapshot.visible_file_count() + } + } + + fn prefix(&self) -> Arc { + if self.snapshot.root_entry().map_or(false, |e| e.is_file()) { + self.snapshot.root_name().into() + } else if self.include_root_name { + format!("{}/", self.snapshot.root_name()).into() + } else { + "".into() + } + } + + fn candidates(&'a self, start: usize) -> Self::Candidates { + PathMatchCandidateSetIter { + traversal: self.snapshot.files(self.include_ignored, start), + } + } +} + +pub struct PathMatchCandidateSetIter<'a> { + traversal: Traversal<'a>, +} + +impl<'a> Iterator for PathMatchCandidateSetIter<'a> { + type Item = fuzzy2::PathMatchCandidate<'a>; + + fn next(&mut self) -> Option { + self.traversal.next().map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + fuzzy2::PathMatchCandidate { + path: &entry.path, + char_bag, + } + } else { + unreachable!() + } + }) + } +} + +impl EventEmitter for Project { + type Event = Event; +} + +impl Entity for Project { + fn app_will_quit( + &mut self, + _: &mut AppContext, + ) -> Option>>> { + let shutdown_futures = self + .language_servers + .drain() + .map(|(_, server_state)| async { + use LanguageServerState::*; + match server_state { + Running { server, .. } => server.shutdown()?.await, + Starting(task) => task.await?.shutdown()?.await, + } + }) + .collect::>(); + + Some( + async move { + futures::future::join_all(shutdown_futures).await; + } + .boxed(), + ) + } +} + +impl> From<(WorktreeId, P)> for ProjectPath { + fn from((worktree_id, path): (WorktreeId, P)) -> Self { + Self { + worktree_id, + path: path.as_ref().into(), + } + } +} + +impl ProjectLspAdapterDelegate { + fn new(project: &Project, cx: &ModelContext) -> Arc { + Arc::new(Self { + project: cx.handle(), + http_client: project.client.http_client(), + }) + } +} + +impl LspAdapterDelegate for ProjectLspAdapterDelegate { + fn show_notification(&self, message: &str, cx: &mut AppContext) { + self.project + .update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned()))); + } + + fn http_client(&self) -> Arc { + self.http_client.clone() + } +} + +fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { + proto::Symbol { + language_server_name: symbol.language_server_name.0.to_string(), + source_worktree_id: symbol.source_worktree_id.to_proto(), + worktree_id: symbol.path.worktree_id.to_proto(), + path: symbol.path.path.to_string_lossy().to_string(), + name: symbol.name.clone(), + kind: unsafe { mem::transmute(symbol.kind) }, + start: Some(proto::PointUtf16 { + row: symbol.range.start.0.row, + column: symbol.range.start.0.column, + }), + end: Some(proto::PointUtf16 { + row: symbol.range.end.0.row, + column: symbol.range.end.0.column, + }), + signature: symbol.signature.to_vec(), + } +} + +fn relativize_path(base: &Path, path: &Path) -> PathBuf { + let mut path_components = path.components(); + let mut base_components = base.components(); + let mut components: Vec = Vec::new(); + loop { + match (path_components.next(), base_components.next()) { + (None, None) => break, + (Some(a), None) => { + components.push(a); + components.extend(path_components.by_ref()); + break; + } + (None, _) => components.push(Component::ParentDir), + (Some(a), Some(b)) if components.is_empty() && a == b => (), + (Some(a), Some(b)) if b == Component::CurDir => components.push(a), + (Some(a), Some(_)) => { + components.push(Component::ParentDir); + for _ in base_components { + components.push(Component::ParentDir); + } + components.push(a); + components.extend(path_components.by_ref()); + break; + } + } + } + components.iter().map(|c| c.as_os_str()).collect() +} + +impl Item for Buffer { + fn entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) + } + + fn project_path(&self, cx: &AppContext) -> Option { + File::from_dyn(self.file()).map(|file| ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path().clone(), + }) + } +} + +async fn wait_for_loading_buffer( + mut receiver: postage::watch::Receiver, Arc>>>, +) -> Result, Arc> { + loop { + if let Some(result) = receiver.borrow().as_ref() { + match result { + Ok(buffer) => return Ok(buffer.to_owned()), + Err(e) => return Err(e.to_owned()), + } + } + receiver.next().await; + } +} + +fn include_text(server: &lsp2::LanguageServer) -> bool { + server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp2::TextDocumentSyncCapability::Kind(_) => None, + lsp2::TextDocumentSyncCapability::Options(options) => options.save.as_ref(), + }) + .and_then(|save_options| match save_options { + lsp2::TextDocumentSyncSaveOptions::Supported(_) => None, + lsp2::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text, + }) + .unwrap_or(false) +} diff --git a/crates/project2/src/project_settings.rs b/crates/project2/src/project_settings.rs new file mode 100644 index 0000000000..607b284813 --- /dev/null +++ b/crates/project2/src/project_settings.rs @@ -0,0 +1,47 @@ +use collections::HashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; +use std::sync::Arc; + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ProjectSettings { + #[serde(default)] + pub lsp: HashMap, LspSettings>, + #[serde(default)] + pub git: GitSettings, +} + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct GitSettings { + pub git_gutter: Option, + pub gutter_debounce: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitGutterSetting { + #[default] + TrackedFiles, + Hide, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct LspSettings { + pub initialization_options: Option, +} + +impl Setting for ProjectSettings { + const KEY: Option<&'static str> = None; + + type FileContent = Self; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs new file mode 100644 index 0000000000..4ca8bb0fa1 --- /dev/null +++ b/crates/project2/src/project_tests.rs @@ -0,0 +1,4077 @@ +// use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; +// use fs::{FakeFs, RealFs}; +// use futures::{future, StreamExt}; +// use gpui::{executor::Deterministic, test::subscribe, AppContext}; +// use language2::{ +// language_settings::{AllLanguageSettings, LanguageSettingsContent}, +// tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, +// LineEnding, OffsetRangeExt, Point, ToPoint, +// }; +// use lsp2::Url; +// use parking_lot::Mutex; +// use pretty_assertions::assert_eq; +// use serde_json::json; +// use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; +// use unindent::Unindent as _; +// use util::{assert_set_eq, test::temp_tree}; + +// #[cfg(test)] +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_symlinks(cx: &mut gpui::TestAppContext) { +// init_test(cx); +// cx.foreground().allow_parking(); + +// let dir = temp_tree(json!({ +// "root": { +// "apple": "", +// "banana": { +// "carrot": { +// "date": "", +// "endive": "", +// } +// }, +// "fennel": { +// "grape": "", +// } +// } +// })); + +// let root_link_path = dir.path().join("root_link"); +// unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap(); +// unix::fs::symlink( +// &dir.path().join("root/fennel"), +// &dir.path().join("root/finnochio"), +// ) +// .unwrap(); + +// let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await; +// project.read_with(cx, |project, cx| { +// let tree = project.worktrees(cx).next().unwrap().read(cx); +// assert_eq!(tree.file_count(), 5); +// assert_eq!( +// tree.inode_for_path("fennel/grape"), +// tree.inode_for_path("finnochio/grape") +// ); +// }); +// } + +// #[gpui::test] +// async fn test_managing_project_specific_settings( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/the-root", +// json!({ +// ".zed": { +// "settings.json": r#"{ "tab_size": 8 }"# +// }, +// "a": { +// "a.rs": "fn a() {\n A\n}" +// }, +// "b": { +// ".zed": { +// "settings.json": r#"{ "tab_size": 2 }"# +// }, +// "b.rs": "fn b() {\n B\n}" +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; +// let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); + +// deterministic.run_until_parked(); +// cx.read(|cx| { +// let tree = worktree.read(cx); + +// let settings_a = language_settings( +// None, +// Some( +// &(File::for_entry( +// tree.entry_for_path("a/a.rs").unwrap().clone(), +// worktree.clone(), +// ) as _), +// ), +// cx, +// ); +// let settings_b = language_settings( +// None, +// Some( +// &(File::for_entry( +// tree.entry_for_path("b/b.rs").unwrap().clone(), +// worktree.clone(), +// ) as _), +// ), +// cx, +// ); + +// assert_eq!(settings_a.tab_size.get(), 8); +// assert_eq!(settings_b.tab_size.get(), 2); +// }); +// } + +// #[gpui::test] +// async fn test_managing_language_servers( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx); + +// let mut rust_language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut json_language = Language::new( +// LanguageConfig { +// name: "JSON".into(), +// path_suffixes: vec!["json".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_rust_servers = rust_language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "the-rust-language-server", +// capabilities: lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![".".to_string(), "::".to_string()]), +// ..Default::default() +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; +// let mut fake_json_servers = json_language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "the-json-language-server", +// capabilities: lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![":".to_string()]), +// ..Default::default() +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/the-root", +// json!({ +// "test.rs": "const A: i32 = 1;", +// "test2.rs": "", +// "Cargo.toml": "a = 1", +// "package.json": "{\"a\": 1}", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + +// // Open a buffer without an associated language server. +// let toml_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/the-root/Cargo.toml", cx) +// }) +// .await +// .unwrap(); + +// // Open a buffer with an associated language server before the language for it has been loaded. +// let rust_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/the-root/test.rs", cx) +// }) +// .await +// .unwrap(); +// rust_buffer.read_with(cx, |buffer, _| { +// assert_eq!(buffer.language().map(|l| l.name()), None); +// }); + +// // Now we add the languages to the project, and ensure they get assigned to all +// // the relevant open buffers. +// project.update(cx, |project, _| { +// project.languages.add(Arc::new(json_language)); +// project.languages.add(Arc::new(rust_language)); +// }); +// deterministic.run_until_parked(); +// rust_buffer.read_with(cx, |buffer, _| { +// assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into())); +// }); + +// // A server is started up, and it is notified about Rust files. +// let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), +// version: 0, +// text: "const A: i32 = 1;".to_string(), +// language_id: Default::default() +// } +// ); + +// // The buffer is configured based on the language server's capabilities. +// rust_buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer.completion_triggers(), +// &[".".to_string(), "::".to_string()] +// ); +// }); +// toml_buffer.read_with(cx, |buffer, _| { +// assert!(buffer.completion_triggers().is_empty()); +// }); + +// // Edit a buffer. The changes are reported to the language server. +// rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx)); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::VersionedTextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), +// 1 +// ) +// ); + +// // Open a third buffer with a different associated language server. +// let json_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/the-root/package.json", cx) +// }) +// .await +// .unwrap(); + +// // A json language server is started up and is only notified about the json buffer. +// let mut fake_json_server = fake_json_servers.next().await.unwrap(); +// assert_eq!( +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(), +// version: 0, +// text: "{\"a\": 1}".to_string(), +// language_id: Default::default() +// } +// ); + +// // This buffer is configured based on the second language server's +// // capabilities. +// json_buffer.read_with(cx, |buffer, _| { +// assert_eq!(buffer.completion_triggers(), &[":".to_string()]); +// }); + +// // When opening another buffer whose language server is already running, +// // it is also configured based on the existing language server's capabilities. +// let rust_buffer2 = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/the-root/test2.rs", cx) +// }) +// .await +// .unwrap(); +// rust_buffer2.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer.completion_triggers(), +// &[".".to_string(), "::".to_string()] +// ); +// }); + +// // Changes are reported only to servers matching the buffer's language. +// toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx)); +// rust_buffer2.update(cx, |buffer, cx| { +// buffer.edit([(0..0, "let x = 1;")], None, cx) +// }); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::VersionedTextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(), +// 1 +// ) +// ); + +// // Save notifications are reported to all servers. +// project +// .update(cx, |project, cx| project.save_buffer(toml_buffer, cx)) +// .await +// .unwrap(); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap() +// ) +// ); +// assert_eq!( +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap() +// ) +// ); + +// // Renames are reported only to servers matching the buffer's language. +// fs.rename( +// Path::new("/the-root/test2.rs"), +// Path::new("/the-root/test3.rs"), +// Default::default(), +// ) +// .await +// .unwrap(); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()), +// ); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(), +// version: 0, +// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), +// language_id: Default::default() +// }, +// ); + +// rust_buffer2.update(cx, |buffer, cx| { +// buffer.update_diagnostics( +// LanguageServerId(0), +// DiagnosticSet::from_sorted_entries( +// vec![DiagnosticEntry { +// diagnostic: Default::default(), +// range: Anchor::MIN..Anchor::MAX, +// }], +// &buffer.snapshot(), +// ), +// cx, +// ); +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, usize>(0..buffer.len(), false) +// .count(), +// 1 +// ); +// }); + +// // When the rename changes the extension of the file, the buffer gets closed on the old +// // language server and gets opened on the new one. +// fs.rename( +// Path::new("/the-root/test3.rs"), +// Path::new("/the-root/test3.json"), +// Default::default(), +// ) +// .await +// .unwrap(); +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),), +// ); +// assert_eq!( +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), +// version: 0, +// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), +// language_id: Default::default() +// }, +// ); + +// // We clear the diagnostics, since the language has changed. +// rust_buffer2.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, usize>(0..buffer.len(), false) +// .count(), +// 0 +// ); +// }); + +// // The renamed file's version resets after changing language server. +// rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx)); +// assert_eq!( +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::VersionedTextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), +// 1 +// ) +// ); + +// // Restart language servers +// project.update(cx, |project, cx| { +// project.restart_language_servers_for_buffers( +// vec![rust_buffer.clone(), json_buffer.clone()], +// cx, +// ); +// }); + +// let mut rust_shutdown_requests = fake_rust_server +// .handle_request::(|_, _| future::ready(Ok(()))); +// let mut json_shutdown_requests = fake_json_server +// .handle_request::(|_, _| future::ready(Ok(()))); +// futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); + +// let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); +// let mut fake_json_server = fake_json_servers.next().await.unwrap(); + +// // Ensure rust document is reopened in new rust language server +// assert_eq!( +// fake_rust_server +// .receive_notification::() +// .await +// .text_document, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(), +// version: 0, +// text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), +// language_id: Default::default() +// } +// ); + +// // Ensure json documents are reopened in new json language server +// assert_set_eq!( +// [ +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// fake_json_server +// .receive_notification::() +// .await +// .text_document, +// ], +// [ +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(), +// version: 0, +// text: json_buffer.read_with(cx, |buffer, _| buffer.text()), +// language_id: Default::default() +// }, +// lsp2::TextDocumentItem { +// uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(), +// version: 0, +// text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), +// language_id: Default::default() +// } +// ] +// ); + +// // Close notifications are reported only to servers matching the buffer's language. +// cx.update(|_| drop(json_buffer)); +// let close_message = lsp2::DidCloseTextDocumentParams { +// text_document: lsp2::TextDocumentIdentifier::new( +// lsp2::Url::from_file_path("/the-root/package.json").unwrap(), +// ), +// }; +// assert_eq!( +// fake_json_server +// .receive_notification::() +// .await, +// close_message, +// ); +// } + +// #[gpui::test] +// async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "the-language-server", +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/the-root", +// json!({ +// ".gitignore": "target\n", +// "src": { +// "a.rs": "", +// "b.rs": "", +// }, +// "target": { +// "x": { +// "out": { +// "x.rs": "" +// } +// }, +// "y": { +// "out": { +// "y.rs": "", +// } +// }, +// "z": { +// "out": { +// "z.rs": "" +// } +// } +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; +// project.update(cx, |project, _| { +// project.languages.add(Arc::new(language)); +// }); +// cx.foreground().run_until_parked(); + +// // Start the language server by opening a buffer with a compatible file extension. +// let _buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/the-root/src/a.rs", cx) +// }) +// .await +// .unwrap(); + +// // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them. +// project.read_with(cx, |project, cx| { +// let worktree = project.worktrees(cx).next().unwrap(); +// assert_eq!( +// worktree +// .read(cx) +// .snapshot() +// .entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) +// .collect::>(), +// &[ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("src"), false), +// (Path::new("src/a.rs"), false), +// (Path::new("src/b.rs"), false), +// (Path::new("target"), true), +// ] +// ); +// }); + +// let prev_read_dir_count = fs.read_dir_call_count(); + +// // Keep track of the FS events reported to the language server. +// let fake_server = fake_servers.next().await.unwrap(); +// let file_changes = Arc::new(Mutex::new(Vec::new())); +// fake_server +// .request::(lsp2::RegistrationParams { +// registrations: vec![lsp2::Registration { +// id: Default::default(), +// method: "workspace/didChangeWatchedFiles".to_string(), +// register_options: serde_json::to_value( +// lsp::DidChangeWatchedFilesRegistrationOptions { +// watchers: vec![ +// lsp2::FileSystemWatcher { +// glob_pattern: lsp2::GlobPattern::String( +// "/the-root/Cargo.toml".to_string(), +// ), +// kind: None, +// }, +// lsp2::FileSystemWatcher { +// glob_pattern: lsp2::GlobPattern::String( +// "/the-root/src/*.{rs,c}".to_string(), +// ), +// kind: None, +// }, +// lsp2::FileSystemWatcher { +// glob_pattern: lsp2::GlobPattern::String( +// "/the-root/target/y/**/*.rs".to_string(), +// ), +// kind: None, +// }, +// ], +// }, +// ) +// .ok(), +// }], +// }) +// .await +// .unwrap(); +// fake_server.handle_notification::({ +// let file_changes = file_changes.clone(); +// move |params, _| { +// let mut file_changes = file_changes.lock(); +// file_changes.extend(params.changes); +// file_changes.sort_by(|a, b| a.uri.cmp(&b.uri)); +// } +// }); + +// cx.foreground().run_until_parked(); +// assert_eq!(mem::take(&mut *file_changes.lock()), &[]); +// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4); + +// // Now the language server has asked us to watch an ignored directory path, +// // so we recursively load it. +// project.read_with(cx, |project, cx| { +// let worktree = project.worktrees(cx).next().unwrap(); +// assert_eq!( +// worktree +// .read(cx) +// .snapshot() +// .entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) +// .collect::>(), +// &[ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("src"), false), +// (Path::new("src/a.rs"), false), +// (Path::new("src/b.rs"), false), +// (Path::new("target"), true), +// (Path::new("target/x"), true), +// (Path::new("target/y"), true), +// (Path::new("target/y/out"), true), +// (Path::new("target/y/out/y.rs"), true), +// (Path::new("target/z"), true), +// ] +// ); +// }); + +// // Perform some file system mutations, two of which match the watched patterns, +// // and one of which does not. +// fs.create_file("/the-root/src/c.rs".as_ref(), Default::default()) +// .await +// .unwrap(); +// fs.create_file("/the-root/src/d.txt".as_ref(), Default::default()) +// .await +// .unwrap(); +// fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) +// .await +// .unwrap(); +// fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) +// .await +// .unwrap(); +// fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default()) +// .await +// .unwrap(); + +// // The language server receives events for the FS mutations that match its watch patterns. +// cx.foreground().run_until_parked(); +// assert_eq!( +// &*file_changes.lock(), +// &[ +// lsp2::FileEvent { +// uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(), +// typ: lsp2::FileChangeType::DELETED, +// }, +// lsp2::FileEvent { +// uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(), +// typ: lsp2::FileChangeType::CREATED, +// }, +// lsp2::FileEvent { +// uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(), +// typ: lsp2::FileChangeType::CREATED, +// }, +// ] +// ); +// } + +// #[gpui::test] +// async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": "let a = 1;", +// "b.rs": "let b = 2;" +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await; + +// let buffer_a = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); +// let buffer_b = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) +// .await +// .unwrap(); + +// project.update(cx, |project, cx| { +// project +// .update_diagnostics( +// LanguageServerId(0), +// lsp::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/a.rs").unwrap(), +// version: None, +// diagnostics: vec![lsp2::Diagnostic { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 4), +// lsp2::Position::new(0, 5), +// ), +// severity: Some(lsp2::DiagnosticSeverity::ERROR), +// message: "error 1".to_string(), +// ..Default::default() +// }], +// }, +// &[], +// cx, +// ) +// .unwrap(); +// project +// .update_diagnostics( +// LanguageServerId(0), +// lsp::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/b.rs").unwrap(), +// version: None, +// diagnostics: vec![lsp2::Diagnostic { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 4), +// lsp2::Position::new(0, 5), +// ), +// severity: Some(lsp2::DiagnosticSeverity::WARNING), +// message: "error 2".to_string(), +// ..Default::default() +// }], +// }, +// &[], +// cx, +// ) +// .unwrap(); +// }); + +// buffer_a.read_with(cx, |buffer, _| { +// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); +// assert_eq!( +// chunks +// .iter() +// .map(|(s, d)| (s.as_str(), *d)) +// .collect::>(), +// &[ +// ("let ", None), +// ("a", Some(DiagnosticSeverity::ERROR)), +// (" = 1;", None), +// ] +// ); +// }); +// buffer_b.read_with(cx, |buffer, _| { +// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); +// assert_eq!( +// chunks +// .iter() +// .map(|(s, d)| (s.as_str(), *d)) +// .collect::>(), +// &[ +// ("let ", None), +// ("b", Some(DiagnosticSeverity::WARNING)), +// (" = 2;", None), +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// "dir": { +// "a.rs": "let a = 1;", +// }, +// "other.rs": "let b = c;" +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/root/dir".as_ref()], cx).await; + +// let (worktree, _) = project +// .update(cx, |project, cx| { +// project.find_or_create_local_worktree("/root/other.rs", false, cx) +// }) +// .await +// .unwrap(); +// let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); + +// project.update(cx, |project, cx| { +// project +// .update_diagnostics( +// LanguageServerId(0), +// lsp::PublishDiagnosticsParams { +// uri: Url::from_file_path("/root/other.rs").unwrap(), +// version: None, +// diagnostics: vec![lsp2::Diagnostic { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 8), +// lsp2::Position::new(0, 9), +// ), +// severity: Some(lsp2::DiagnosticSeverity::ERROR), +// message: "unknown variable 'c'".to_string(), +// ..Default::default() +// }], +// }, +// &[], +// cx, +// ) +// .unwrap(); +// }); + +// let buffer = project +// .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) +// .await +// .unwrap(); +// buffer.read_with(cx, |buffer, _| { +// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); +// assert_eq!( +// chunks +// .iter() +// .map(|(s, d)| (s.as_str(), *d)) +// .collect::>(), +// &[ +// ("let b = ", None), +// ("c", Some(DiagnosticSeverity::ERROR)), +// (";", None), +// ] +// ); +// }); + +// project.read_with(cx, |project, cx| { +// assert_eq!(project.diagnostic_summaries(cx).next(), None); +// assert_eq!(project.diagnostic_summary(cx).error_count, 0); +// }); +// } + +// #[gpui::test] +// async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let progress_token = "the-progress-token"; +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// disk_based_diagnostics_progress_token: Some(progress_token.into()), +// disk_based_diagnostics_sources: vec!["disk".into()], +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": "fn a() { A }", +// "b.rs": "const y: i32 = 1", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let worktree_id = project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id()); + +// // Cause worktree to start the fake language server +// let _buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) +// .await +// .unwrap(); + +// let mut events = subscribe(&project, cx); + +// let fake_server = fake_servers.next().await.unwrap(); +// assert_eq!( +// events.next().await.unwrap(), +// Event::LanguageServerAdded(LanguageServerId(0)), +// ); + +// fake_server +// .start_progress(format!("{}/0", progress_token)) +// .await; +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiskBasedDiagnosticsStarted { +// language_server_id: LanguageServerId(0), +// } +// ); + +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/a.rs").unwrap(), +// version: None, +// diagnostics: vec![lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), +// severity: Some(lsp2::DiagnosticSeverity::ERROR), +// message: "undefined variable 'A'".to_string(), +// ..Default::default() +// }], +// }); +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiagnosticsUpdated { +// language_server_id: LanguageServerId(0), +// path: (worktree_id, Path::new("a.rs")).into() +// } +// ); + +// fake_server.end_progress(format!("{}/0", progress_token)); +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiskBasedDiagnosticsFinished { +// language_server_id: LanguageServerId(0) +// } +// ); + +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// buffer.read_with(cx, |buffer, _| { +// let snapshot = buffer.snapshot(); +// let diagnostics = snapshot +// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) +// .collect::>(); +// assert_eq!( +// diagnostics, +// &[DiagnosticEntry { +// range: Point::new(0, 9)..Point::new(0, 10), +// diagnostic: Diagnostic { +// severity: lsp2::DiagnosticSeverity::ERROR, +// message: "undefined variable 'A'".to_string(), +// group_id: 0, +// is_primary: true, +// ..Default::default() +// } +// }] +// ) +// }); + +// // Ensure publishing empty diagnostics twice only results in one update event. +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/a.rs").unwrap(), +// version: None, +// diagnostics: Default::default(), +// }); +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiagnosticsUpdated { +// language_server_id: LanguageServerId(0), +// path: (worktree_id, Path::new("a.rs")).into() +// } +// ); + +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/a.rs").unwrap(), +// version: None, +// diagnostics: Default::default(), +// }); +// cx.foreground().run_until_parked(); +// assert_eq!(futures::poll!(events.next()), Poll::Pending); +// } + +// #[gpui::test] +// async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let progress_token = "the-progress-token"; +// let mut language = Language::new( +// LanguageConfig { +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// disk_based_diagnostics_sources: vec!["disk".into()], +// disk_based_diagnostics_progress_token: Some(progress_token.into()), +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); + +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// // Simulate diagnostics starting to update. +// let fake_server = fake_servers.next().await.unwrap(); +// fake_server.start_progress(progress_token).await; + +// // Restart the server before the diagnostics finish updating. +// project.update(cx, |project, cx| { +// project.restart_language_servers_for_buffers([buffer], cx); +// }); +// let mut events = subscribe(&project, cx); + +// // Simulate the newly started server sending more diagnostics. +// let fake_server = fake_servers.next().await.unwrap(); +// assert_eq!( +// events.next().await.unwrap(), +// Event::LanguageServerAdded(LanguageServerId(1)) +// ); +// fake_server.start_progress(progress_token).await; +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiskBasedDiagnosticsStarted { +// language_server_id: LanguageServerId(1) +// } +// ); +// project.read_with(cx, |project, _| { +// assert_eq!( +// project +// .language_servers_running_disk_based_diagnostics() +// .collect::>(), +// [LanguageServerId(1)] +// ); +// }); + +// // All diagnostics are considered done, despite the old server's diagnostic +// // task never completing. +// fake_server.end_progress(progress_token); +// assert_eq!( +// events.next().await.unwrap(), +// Event::DiskBasedDiagnosticsFinished { +// language_server_id: LanguageServerId(1) +// } +// ); +// project.read_with(cx, |project, _| { +// assert_eq!( +// project +// .language_servers_running_disk_based_diagnostics() +// .collect::>(), +// [LanguageServerId(0); 0] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); + +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// // Publish diagnostics +// let fake_server = fake_servers.next().await.unwrap(); +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: Url::from_file_path("/dir/a.rs").unwrap(), +// version: None, +// diagnostics: vec![lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)), +// severity: Some(lsp2::DiagnosticSeverity::ERROR), +// message: "the message".to_string(), +// ..Default::default() +// }], +// }); + +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, usize>(0..1, false) +// .map(|entry| entry.diagnostic.message.clone()) +// .collect::>(), +// ["the message".to_string()] +// ); +// }); +// project.read_with(cx, |project, cx| { +// assert_eq!( +// project.diagnostic_summary(cx), +// DiagnosticSummary { +// error_count: 1, +// warning_count: 0, +// } +// ); +// }); + +// project.update(cx, |project, cx| { +// project.restart_language_servers_for_buffers([buffer.clone()], cx); +// }); + +// // The diagnostics are cleared. +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, usize>(0..1, false) +// .map(|entry| entry.diagnostic.message.clone()) +// .collect::>(), +// Vec::::new(), +// ); +// }); +// project.read_with(cx, |project, cx| { +// assert_eq!( +// project.diagnostic_summary(cx), +// DiagnosticSummary { +// error_count: 0, +// warning_count: 0, +// } +// ); +// }); +// } + +// #[gpui::test] +// async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "the-lsp", +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": "" })).await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); + +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// // Before restarting the server, report diagnostics with an unknown buffer version. +// let fake_server = fake_servers.next().await.unwrap(); +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), +// version: Some(10000), +// diagnostics: Vec::new(), +// }); +// cx.foreground().run_until_parked(); + +// project.update(cx, |project, cx| { +// project.restart_language_servers_for_buffers([buffer.clone()], cx); +// }); +// let mut fake_server = fake_servers.next().await.unwrap(); +// let notification = fake_server +// .receive_notification::() +// .await +// .text_document; +// assert_eq!(notification.version, 0); +// } + +// #[gpui::test] +// async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut rust = Language::new( +// LanguageConfig { +// name: Arc::from("Rust"), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_rust_servers = rust +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "rust-lsp", +// ..Default::default() +// })) +// .await; +// let mut js = Language::new( +// LanguageConfig { +// name: Arc::from("JavaScript"), +// path_suffixes: vec!["js".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_js_servers = js +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// name: "js-lsp", +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| { +// project.languages.add(Arc::new(rust)); +// project.languages.add(Arc::new(js)); +// }); + +// let _rs_buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); +// let _js_buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx)) +// .await +// .unwrap(); + +// let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap(); +// assert_eq!( +// fake_rust_server_1 +// .receive_notification::() +// .await +// .text_document +// .uri +// .as_str(), +// "file:///dir/a.rs" +// ); + +// let mut fake_js_server = fake_js_servers.next().await.unwrap(); +// assert_eq!( +// fake_js_server +// .receive_notification::() +// .await +// .text_document +// .uri +// .as_str(), +// "file:///dir/b.js" +// ); + +// // Disable Rust language server, ensuring only that server gets stopped. +// cx.update(|cx| { +// cx.update_global(|settings: &mut SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.languages.insert( +// Arc::from("Rust"), +// LanguageSettingsContent { +// enable_language_server: Some(false), +// ..Default::default() +// }, +// ); +// }); +// }) +// }); +// fake_rust_server_1 +// .receive_notification::() +// .await; + +// // Enable Rust and disable JavaScript language servers, ensuring that the +// // former gets started again and that the latter stops. +// cx.update(|cx| { +// cx.update_global(|settings: &mut SettingsStore, cx| { +// settings.update_user_settings::(cx, |settings| { +// settings.languages.insert( +// Arc::from("Rust"), +// LanguageSettingsContent { +// enable_language_server: Some(true), +// ..Default::default() +// }, +// ); +// settings.languages.insert( +// Arc::from("JavaScript"), +// LanguageSettingsContent { +// enable_language_server: Some(false), +// ..Default::default() +// }, +// ); +// }); +// }) +// }); +// let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); +// assert_eq!( +// fake_rust_server_2 +// .receive_notification::() +// .await +// .text_document +// .uri +// .as_str(), +// "file:///dir/a.rs" +// ); +// fake_js_server +// .receive_notification::() +// .await; +// } + +// #[gpui::test(iterations = 3)] +// async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// disk_based_diagnostics_sources: vec!["disk".into()], +// ..Default::default() +// })) +// .await; + +// let text = " +// fn a() { A } +// fn b() { BB } +// fn c() { CCC } +// " +// .unindent(); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": text })).await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); + +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// let mut fake_server = fake_servers.next().await.unwrap(); +// let open_notification = fake_server +// .receive_notification::() +// .await; + +// // Edit the buffer, moving the content down +// buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx)); +// let change_notification_1 = fake_server +// .receive_notification::() +// .await; +// assert!(change_notification_1.text_document.version > open_notification.text_document.version); + +// // Report some diagnostics for the initial version of the buffer +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), +// version: Some(open_notification.text_document.version), +// diagnostics: vec![ +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), +// severity: Some(DiagnosticSeverity::ERROR), +// message: "undefined variable 'A'".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)), +// severity: Some(DiagnosticSeverity::ERROR), +// message: "undefined variable 'BB'".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)), +// severity: Some(DiagnosticSeverity::ERROR), +// source: Some("disk".to_string()), +// message: "undefined variable 'CCC'".to_string(), +// ..Default::default() +// }, +// ], +// }); + +// // The diagnostics have moved down since they were created. +// buffer.next_notification(cx).await; +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false) +// .collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(3, 9)..Point::new(3, 11), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::ERROR, +// message: "undefined variable 'BB'".to_string(), +// is_disk_based: true, +// group_id: 1, +// is_primary: true, +// ..Default::default() +// }, +// }, +// DiagnosticEntry { +// range: Point::new(4, 9)..Point::new(4, 12), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::ERROR, +// message: "undefined variable 'CCC'".to_string(), +// is_disk_based: true, +// group_id: 2, +// is_primary: true, +// ..Default::default() +// } +// } +// ] +// ); +// assert_eq!( +// chunks_with_diagnostics(buffer, 0..buffer.len()), +// [ +// ("\n\nfn a() { ".to_string(), None), +// ("A".to_string(), Some(DiagnosticSeverity::ERROR)), +// (" }\nfn b() { ".to_string(), None), +// ("BB".to_string(), Some(DiagnosticSeverity::ERROR)), +// (" }\nfn c() { ".to_string(), None), +// ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)), +// (" }\n".to_string(), None), +// ] +// ); +// assert_eq!( +// chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)), +// [ +// ("B".to_string(), Some(DiagnosticSeverity::ERROR)), +// (" }\nfn c() { ".to_string(), None), +// ("CC".to_string(), Some(DiagnosticSeverity::ERROR)), +// ] +// ); +// }); + +// // Ensure overlapping diagnostics are highlighted correctly. +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), +// version: Some(open_notification.text_document.version), +// diagnostics: vec![ +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), +// severity: Some(DiagnosticSeverity::ERROR), +// message: "undefined variable 'A'".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)), +// severity: Some(DiagnosticSeverity::WARNING), +// message: "unreachable statement".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// ], +// }); + +// buffer.next_notification(cx).await; +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false) +// .collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(2, 9)..Point::new(2, 12), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::WARNING, +// message: "unreachable statement".to_string(), +// is_disk_based: true, +// group_id: 4, +// is_primary: true, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(2, 9)..Point::new(2, 10), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::ERROR, +// message: "undefined variable 'A'".to_string(), +// is_disk_based: true, +// group_id: 3, +// is_primary: true, +// ..Default::default() +// }, +// } +// ] +// ); +// assert_eq!( +// chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)), +// [ +// ("fn a() { ".to_string(), None), +// ("A".to_string(), Some(DiagnosticSeverity::ERROR)), +// (" }".to_string(), Some(DiagnosticSeverity::WARNING)), +// ("\n".to_string(), None), +// ] +// ); +// assert_eq!( +// chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)), +// [ +// (" }".to_string(), Some(DiagnosticSeverity::WARNING)), +// ("\n".to_string(), None), +// ] +// ); +// }); + +// // Keep editing the buffer and ensure disk-based diagnostics get translated according to the +// // changes since the last save. +// buffer.update(cx, |buffer, cx| { +// buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); +// buffer.edit( +// [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")], +// None, +// cx, +// ); +// buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx); +// }); +// let change_notification_2 = fake_server +// .receive_notification::() +// .await; +// assert!( +// change_notification_2.text_document.version > change_notification_1.text_document.version +// ); + +// // Handle out-of-order diagnostics +// fake_server.notify::(lsp2::PublishDiagnosticsParams { +// uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(), +// version: Some(change_notification_2.text_document.version), +// diagnostics: vec![ +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)), +// severity: Some(DiagnosticSeverity::ERROR), +// message: "undefined variable 'BB'".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), +// severity: Some(DiagnosticSeverity::WARNING), +// message: "undefined variable 'A'".to_string(), +// source: Some("disk".to_string()), +// ..Default::default() +// }, +// ], +// }); + +// buffer.next_notification(cx).await; +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert_eq!( +// buffer +// .snapshot() +// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) +// .collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(2, 21)..Point::new(2, 22), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::WARNING, +// message: "undefined variable 'A'".to_string(), +// is_disk_based: true, +// group_id: 6, +// is_primary: true, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(3, 9)..Point::new(3, 14), +// diagnostic: Diagnostic { +// source: Some("disk".into()), +// severity: DiagnosticSeverity::ERROR, +// message: "undefined variable 'BB'".to_string(), +// is_disk_based: true, +// group_id: 5, +// is_primary: true, +// ..Default::default() +// }, +// } +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let text = concat!( +// "let one = ;\n", // +// "let two = \n", +// "let three = 3;\n", +// ); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": text })).await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// project.update(cx, |project, cx| { +// project +// .update_buffer_diagnostics( +// &buffer, +// LanguageServerId(0), +// None, +// vec![ +// DiagnosticEntry { +// range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// message: "syntax error 1".to_string(), +// ..Default::default() +// }, +// }, +// DiagnosticEntry { +// range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// message: "syntax error 2".to_string(), +// ..Default::default() +// }, +// }, +// ], +// cx, +// ) +// .unwrap(); +// }); + +// // An empty range is extended forward to include the following character. +// // At the end of a line, an empty range is extended backward to include +// // the preceding character. +// buffer.read_with(cx, |buffer, _| { +// let chunks = chunks_with_diagnostics(buffer, 0..buffer.len()); +// assert_eq!( +// chunks +// .iter() +// .map(|(s, d)| (s.as_str(), *d)) +// .collect::>(), +// &[ +// ("let one = ", None), +// (";", Some(DiagnosticSeverity::ERROR)), +// ("\nlet two =", None), +// (" ", Some(DiagnosticSeverity::ERROR)), +// ("\nlet three = 3;\n", None) +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; + +// project.update(cx, |project, cx| { +// project +// .update_diagnostic_entries( +// LanguageServerId(0), +// Path::new("/dir/a.rs").to_owned(), +// None, +// vec![DiagnosticEntry { +// range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// is_primary: true, +// message: "syntax error a1".to_string(), +// ..Default::default() +// }, +// }], +// cx, +// ) +// .unwrap(); +// project +// .update_diagnostic_entries( +// LanguageServerId(1), +// Path::new("/dir/a.rs").to_owned(), +// None, +// vec![DiagnosticEntry { +// range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// is_primary: true, +// message: "syntax error b1".to_string(), +// ..Default::default() +// }, +// }], +// cx, +// ) +// .unwrap(); + +// assert_eq!( +// project.diagnostic_summary(cx), +// DiagnosticSummary { +// error_count: 2, +// warning_count: 0, +// } +// ); +// }); +// } + +// #[gpui::test] +// async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; + +// let text = " +// fn a() { +// f1(); +// } +// fn b() { +// f2(); +// } +// fn c() { +// f3(); +// } +// " +// .unindent(); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": text.clone(), +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// let mut fake_server = fake_servers.next().await.unwrap(); +// let lsp_document_version = fake_server +// .receive_notification::() +// .await +// .text_document +// .version; + +// // Simulate editing the buffer after the language server computes some edits. +// buffer.update(cx, |buffer, cx| { +// buffer.edit( +// [( +// Point::new(0, 0)..Point::new(0, 0), +// "// above first function\n", +// )], +// None, +// cx, +// ); +// buffer.edit( +// [( +// Point::new(2, 0)..Point::new(2, 0), +// " // inside first function\n", +// )], +// None, +// cx, +// ); +// buffer.edit( +// [( +// Point::new(6, 4)..Point::new(6, 4), +// "// inside second function ", +// )], +// None, +// cx, +// ); + +// assert_eq!( +// buffer.text(), +// " +// // above first function +// fn a() { +// // inside first function +// f1(); +// } +// fn b() { +// // inside second function f2(); +// } +// fn c() { +// f3(); +// } +// " +// .unindent() +// ); +// }); + +// let edits = project +// .update(cx, |project, cx| { +// project.edits_from_lsp( +// &buffer, +// vec![ +// // replace body of first function +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 0), +// lsp2::Position::new(3, 0), +// ), +// new_text: " +// fn a() { +// f10(); +// } +// " +// .unindent(), +// }, +// // edit inside second function +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(4, 6), +// lsp2::Position::new(4, 6), +// ), +// new_text: "00".into(), +// }, +// // edit inside third function via two distinct edits +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(7, 5), +// lsp2::Position::new(7, 5), +// ), +// new_text: "4000".into(), +// }, +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(7, 5), +// lsp2::Position::new(7, 6), +// ), +// new_text: "".into(), +// }, +// ], +// LanguageServerId(0), +// Some(lsp_document_version), +// cx, +// ) +// }) +// .await +// .unwrap(); + +// buffer.update(cx, |buffer, cx| { +// for (range, new_text) in edits { +// buffer.edit([(range, new_text)], None, cx); +// } +// assert_eq!( +// buffer.text(), +// " +// // above first function +// fn a() { +// // inside first function +// f10(); +// } +// fn b() { +// // inside second function f200(); +// } +// fn c() { +// f4000(); +// } +// " +// .unindent() +// ); +// }); +// } + +// #[gpui::test] +// async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let text = " +// use a::b; +// use a::c; + +// fn f() { +// b(); +// c(); +// } +// " +// .unindent(); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": text.clone(), +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// // Simulate the language server sending us a small edit in the form of a very large diff. +// // Rust-analyzer does this when performing a merge-imports code action. +// let edits = project +// .update(cx, |project, cx| { +// project.edits_from_lsp( +// &buffer, +// [ +// // Replace the first use statement without editing the semicolon. +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 4), +// lsp2::Position::new(0, 8), +// ), +// new_text: "a::{b, c}".into(), +// }, +// // Reinsert the remainder of the file between the semicolon and the final +// // newline of the file. +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 9), +// lsp2::Position::new(0, 9), +// ), +// new_text: "\n\n".into(), +// }, +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 9), +// lsp2::Position::new(0, 9), +// ), +// new_text: " +// fn f() { +// b(); +// c(); +// }" +// .unindent(), +// }, +// // Delete everything after the first newline of the file. +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(1, 0), +// lsp2::Position::new(7, 0), +// ), +// new_text: "".into(), +// }, +// ], +// LanguageServerId(0), +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); + +// buffer.update(cx, |buffer, cx| { +// let edits = edits +// .into_iter() +// .map(|(range, text)| { +// ( +// range.start.to_point(buffer)..range.end.to_point(buffer), +// text, +// ) +// }) +// .collect::>(); + +// assert_eq!( +// edits, +// [ +// (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), +// (Point::new(1, 0)..Point::new(2, 0), "".into()) +// ] +// ); + +// for (range, new_text) in edits { +// buffer.edit([(range, new_text)], None, cx); +// } +// assert_eq!( +// buffer.text(), +// " +// use a::{b, c}; + +// fn f() { +// b(); +// c(); +// } +// " +// .unindent() +// ); +// }); +// } + +// #[gpui::test] +// async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let text = " +// use a::b; +// use a::c; + +// fn f() { +// b(); +// c(); +// } +// " +// .unindent(); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": text.clone(), +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx)) +// .await +// .unwrap(); + +// // Simulate the language server sending us edits in a non-ordered fashion, +// // with ranges sometimes being inverted or pointing to invalid locations. +// let edits = project +// .update(cx, |project, cx| { +// project.edits_from_lsp( +// &buffer, +// [ +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 9), +// lsp2::Position::new(0, 9), +// ), +// new_text: "\n\n".into(), +// }, +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 8), +// lsp2::Position::new(0, 4), +// ), +// new_text: "a::{b, c}".into(), +// }, +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(1, 0), +// lsp2::Position::new(99, 0), +// ), +// new_text: "".into(), +// }, +// lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 9), +// lsp2::Position::new(0, 9), +// ), +// new_text: " +// fn f() { +// b(); +// c(); +// }" +// .unindent(), +// }, +// ], +// LanguageServerId(0), +// None, +// cx, +// ) +// }) +// .await +// .unwrap(); + +// buffer.update(cx, |buffer, cx| { +// let edits = edits +// .into_iter() +// .map(|(range, text)| { +// ( +// range.start.to_point(buffer)..range.end.to_point(buffer), +// text, +// ) +// }) +// .collect::>(); + +// assert_eq!( +// edits, +// [ +// (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()), +// (Point::new(1, 0)..Point::new(2, 0), "".into()) +// ] +// ); + +// for (range, new_text) in edits { +// buffer.edit([(range, new_text)], None, cx); +// } +// assert_eq!( +// buffer.text(), +// " +// use a::{b, c}; + +// fn f() { +// b(); +// c(); +// } +// " +// .unindent() +// ); +// }); +// } + +// fn chunks_with_diagnostics( +// buffer: &Buffer, +// range: Range, +// ) -> Vec<(String, Option)> { +// let mut chunks: Vec<(String, Option)> = Vec::new(); +// for chunk in buffer.snapshot().chunks(range, true) { +// if chunks.last().map_or(false, |prev_chunk| { +// prev_chunk.1 == chunk.diagnostic_severity +// }) { +// chunks.last_mut().unwrap().0.push_str(chunk.text); +// } else { +// chunks.push((chunk.text.to_string(), chunk.diagnostic_severity)); +// } +// } +// chunks +// } + +// #[gpui::test(iterations = 10)] +// async fn test_definition(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.rs": "const fn a() { A }", +// "b.rs": "const y: i32 = crate::a()", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); + +// let buffer = project +// .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx)) +// .await +// .unwrap(); + +// let fake_server = fake_servers.next().await.unwrap(); +// fake_server.handle_request::(|params, _| async move { +// let params = params.text_document_position_params; +// assert_eq!( +// params.text_document.uri.to_file_path().unwrap(), +// Path::new("/dir/b.rs"), +// ); +// assert_eq!(params.position, lsp2::Position::new(0, 22)); + +// Ok(Some(lsp2::GotoDefinitionResponse::Scalar( +// lsp2::Location::new( +// lsp2::Url::from_file_path("/dir/a.rs").unwrap(), +// lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)), +// ), +// ))) +// }); + +// let mut definitions = project +// .update(cx, |project, cx| project.definition(&buffer, 22, cx)) +// .await +// .unwrap(); + +// // Assert no new language server started +// cx.foreground().run_until_parked(); +// assert!(fake_servers.try_next().is_err()); + +// assert_eq!(definitions.len(), 1); +// let definition = definitions.pop().unwrap(); +// cx.update(|cx| { +// let target_buffer = definition.target.buffer.read(cx); +// assert_eq!( +// target_buffer +// .file() +// .unwrap() +// .as_local() +// .unwrap() +// .abs_path(cx), +// Path::new("/dir/a.rs"), +// ); +// assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); +// assert_eq!( +// list_worktrees(&project, cx), +// [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)] +// ); + +// drop(definition); +// }); +// cx.read(|cx| { +// assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); +// }); + +// fn list_worktrees<'a>( +// project: &'a ModelHandle, +// cx: &'a AppContext, +// ) -> Vec<(&'a Path, bool)> { +// project +// .read(cx) +// .worktrees(cx) +// .map(|worktree| { +// let worktree = worktree.read(cx); +// ( +// worktree.as_local().unwrap().abs_path().as_ref(), +// worktree.is_visible(), +// ) +// }) +// .collect::>() +// } +// } + +// #[gpui::test] +// async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "TypeScript".into(), +// path_suffixes: vec!["ts".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_typescript()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![":".to_string()]), +// ..Default::default() +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.ts": "", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) +// .await +// .unwrap(); + +// let fake_server = fake_language_servers.next().await.unwrap(); + +// let text = "let a = b.fqn"; +// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); +// let completions = project.update(cx, |project, cx| { +// project.completions(&buffer, text.len(), cx) +// }); + +// fake_server +// .handle_request::(|_, _| async move { +// Ok(Some(lsp2::CompletionResponse::Array(vec![ +// lsp2::CompletionItem { +// label: "fullyQualifiedName?".into(), +// insert_text: Some("fullyQualifiedName".into()), +// ..Default::default() +// }, +// ]))) +// }) +// .next() +// .await; +// let completions = completions.await.unwrap(); +// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); +// assert_eq!(completions.len(), 1); +// assert_eq!(completions[0].new_text, "fullyQualifiedName"); +// assert_eq!( +// completions[0].old_range.to_offset(&snapshot), +// text.len() - 3..text.len() +// ); + +// let text = "let a = \"atoms/cmp\""; +// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); +// let completions = project.update(cx, |project, cx| { +// project.completions(&buffer, text.len() - 1, cx) +// }); + +// fake_server +// .handle_request::(|_, _| async move { +// Ok(Some(lsp2::CompletionResponse::Array(vec![ +// lsp2::CompletionItem { +// label: "component".into(), +// ..Default::default() +// }, +// ]))) +// }) +// .next() +// .await; +// let completions = completions.await.unwrap(); +// let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); +// assert_eq!(completions.len(), 1); +// assert_eq!(completions[0].new_text, "component"); +// assert_eq!( +// completions[0].old_range.to_offset(&snapshot), +// text.len() - 4..text.len() - 1 +// ); +// } + +// #[gpui::test] +// async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "TypeScript".into(), +// path_suffixes: vec!["ts".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_typescript::language_typescript()), +// ); +// let mut fake_language_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp::ServerCapabilities { +// completion_provider: Some(lsp::CompletionOptions { +// trigger_characters: Some(vec![":".to_string()]), +// ..Default::default() +// }), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.ts": "", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) +// .await +// .unwrap(); + +// let fake_server = fake_language_servers.next().await.unwrap(); + +// let text = "let a = b.fqn"; +// buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); +// let completions = project.update(cx, |project, cx| { +// project.completions(&buffer, text.len(), cx) +// }); + +// fake_server +// .handle_request::(|_, _| async move { +// Ok(Some(lsp2::CompletionResponse::Array(vec![ +// lsp2::CompletionItem { +// label: "fullyQualifiedName?".into(), +// insert_text: Some("fully\rQualified\r\nName".into()), +// ..Default::default() +// }, +// ]))) +// }) +// .next() +// .await; +// let completions = completions.await.unwrap(); +// assert_eq!(completions.len(), 1); +// assert_eq!(completions[0].new_text, "fully\nQualified\nName"); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "TypeScript".into(), +// path_suffixes: vec!["ts".to_string()], +// ..Default::default() +// }, +// None, +// ); +// let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.ts": "a", +// }), +// ) +// .await; + +// let project = Project::test(fs, ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) +// .await +// .unwrap(); + +// let fake_server = fake_language_servers.next().await.unwrap(); + +// // Language server returns code actions that contain commands, and not edits. +// let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); +// fake_server +// .handle_request::(|_, _| async move { +// Ok(Some(vec![ +// lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction { +// title: "The code action".into(), +// command: Some(lsp::Command { +// title: "The command".into(), +// command: "_the/command".into(), +// arguments: Some(vec![json!("the-argument")]), +// }), +// ..Default::default() +// }), +// lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction { +// title: "two".into(), +// ..Default::default() +// }), +// ])) +// }) +// .next() +// .await; + +// let action = actions.await.unwrap()[0].clone(); +// let apply = project.update(cx, |project, cx| { +// project.apply_code_action(buffer.clone(), action, true, cx) +// }); + +// // Resolving the code action does not populate its edits. In absence of +// // edits, we must execute the given command. +// fake_server.handle_request::( +// |action, _| async move { Ok(action) }, +// ); + +// // While executing the command, the language server sends the editor +// // a `workspaceEdit` request. +// fake_server +// .handle_request::({ +// let fake = fake_server.clone(); +// move |params, _| { +// assert_eq!(params.command, "_the/command"); +// let fake = fake.clone(); +// async move { +// fake.server +// .request::( +// lsp2::ApplyWorkspaceEditParams { +// label: None, +// edit: lsp::WorkspaceEdit { +// changes: Some( +// [( +// lsp2::Url::from_file_path("/dir/a.ts").unwrap(), +// vec![lsp2::TextEdit { +// range: lsp2::Range::new( +// lsp2::Position::new(0, 0), +// lsp2::Position::new(0, 0), +// ), +// new_text: "X".into(), +// }], +// )] +// .into_iter() +// .collect(), +// ), +// ..Default::default() +// }, +// }, +// ) +// .await +// .unwrap(); +// Ok(Some(json!(null))) +// } +// } +// }) +// .next() +// .await; + +// // Applying the code action returns a project transaction containing the edits +// // sent by the language server in its `workspaceEdit` request. +// let transaction = apply.await.unwrap(); +// assert!(transaction.0.contains_key(&buffer)); +// buffer.update(cx, |buffer, cx| { +// assert_eq!(buffer.text(), "Xa"); +// buffer.undo(cx); +// assert_eq!(buffer.text(), "a"); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_save_file(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "file1": "the old contents", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) +// .await +// .unwrap(); +// buffer.update(cx, |buffer, cx| { +// assert_eq!(buffer.text(), "the old contents"); +// buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); +// }); + +// project +// .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) +// .await +// .unwrap(); + +// let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); +// assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); +// } + +// #[gpui::test] +// async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "file1": "the old contents", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await; +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) +// .await +// .unwrap(); +// buffer.update(cx, |buffer, cx| { +// buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx); +// }); + +// project +// .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) +// .await +// .unwrap(); + +// let new_text = fs.load(Path::new("/dir/file1")).await.unwrap(); +// assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text())); +// } + +// #[gpui::test] +// async fn test_save_as(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree("/dir", json!({})).await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// let languages = project.read_with(cx, |project, _| project.languages().clone()); +// languages.register( +// "/some/path", +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".into()], +// ..Default::default() +// }, +// tree_sitter_rust::language(), +// vec![], +// |_| Default::default(), +// ); + +// let buffer = project.update(cx, |project, cx| { +// project.create_buffer("", None, cx).unwrap() +// }); +// buffer.update(cx, |buffer, cx| { +// buffer.edit([(0..0, "abc")], None, cx); +// assert!(buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text"); +// }); +// project +// .update(cx, |project, cx| { +// project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) +// }) +// .await +// .unwrap(); +// assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc"); + +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, cx| { +// assert_eq!( +// buffer.file().unwrap().full_path(cx), +// Path::new("dir/file1.rs") +// ); +// assert!(!buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust"); +// }); + +// let opened_buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/dir/file1.rs", cx) +// }) +// .await +// .unwrap(); +// assert_eq!(opened_buffer, buffer); +// } + +// #[gpui::test(retries = 5)] +// async fn test_rescan_and_remote_updates( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx); +// cx.foreground().allow_parking(); + +// let dir = temp_tree(json!({ +// "a": { +// "file1": "", +// "file2": "", +// "file3": "", +// }, +// "b": { +// "c": { +// "file4": "", +// "file5": "", +// } +// } +// })); + +// let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await; +// let rpc = project.read_with(cx, |p, _| p.client.clone()); + +// let buffer_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| { +// let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx)); +// async move { buffer.await.unwrap() } +// }; +// let id_for_path = |path: &'static str, cx: &gpui2::TestAppContext| { +// project.read_with(cx, |project, cx| { +// let tree = project.worktrees(cx).next().unwrap(); +// tree.read(cx) +// .entry_for_path(path) +// .unwrap_or_else(|| panic!("no entry for path {}", path)) +// .id +// }) +// }; + +// let buffer2 = buffer_for_path("a/file2", cx).await; +// let buffer3 = buffer_for_path("a/file3", cx).await; +// let buffer4 = buffer_for_path("b/c/file4", cx).await; +// let buffer5 = buffer_for_path("b/c/file5", cx).await; + +// let file2_id = id_for_path("a/file2", cx); +// let file3_id = id_for_path("a/file3", cx); +// let file4_id = id_for_path("b/c/file4", cx); + +// // Create a remote copy of this worktree. +// let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); + +// let metadata = tree.read_with(cx, |tree, _| tree.as_local().unwrap().metadata_proto()); + +// let updates = Arc::new(Mutex::new(Vec::new())); +// tree.update(cx, |tree, cx| { +// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { +// let updates = updates.clone(); +// move |update| { +// updates.lock().push(update); +// async { true } +// } +// }); +// }); + +// let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx)); +// deterministic.run_until_parked(); + +// cx.read(|cx| { +// assert!(!buffer2.read(cx).is_dirty()); +// assert!(!buffer3.read(cx).is_dirty()); +// assert!(!buffer4.read(cx).is_dirty()); +// assert!(!buffer5.read(cx).is_dirty()); +// }); + +// // Rename and delete files and directories. +// tree.flush_fs_events(cx).await; +// std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); +// std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); +// std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); +// std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); +// tree.flush_fs_events(cx).await; + +// let expected_paths = vec![ +// "a", +// "a/file1", +// "a/file2.new", +// "b", +// "d", +// "d/file3", +// "d/file4", +// ]; + +// cx.read(|app| { +// assert_eq!( +// tree.read(app) +// .paths() +// .map(|p| p.to_str().unwrap()) +// .collect::>(), +// expected_paths +// ); + +// assert_eq!(id_for_path("a/file2.new", cx), file2_id); +// assert_eq!(id_for_path("d/file3", cx), file3_id); +// assert_eq!(id_for_path("d/file4", cx), file4_id); + +// assert_eq!( +// buffer2.read(app).file().unwrap().path().as_ref(), +// Path::new("a/file2.new") +// ); +// assert_eq!( +// buffer3.read(app).file().unwrap().path().as_ref(), +// Path::new("d/file3") +// ); +// assert_eq!( +// buffer4.read(app).file().unwrap().path().as_ref(), +// Path::new("d/file4") +// ); +// assert_eq!( +// buffer5.read(app).file().unwrap().path().as_ref(), +// Path::new("b/c/file5") +// ); + +// assert!(!buffer2.read(app).file().unwrap().is_deleted()); +// assert!(!buffer3.read(app).file().unwrap().is_deleted()); +// assert!(!buffer4.read(app).file().unwrap().is_deleted()); +// assert!(buffer5.read(app).file().unwrap().is_deleted()); +// }); + +// // Update the remote worktree. Check that it becomes consistent with the +// // local worktree. +// deterministic.run_until_parked(); +// remote.update(cx, |remote, _| { +// for update in updates.lock().drain(..) { +// remote.as_remote_mut().unwrap().update_from_remote(update); +// } +// }); +// deterministic.run_until_parked(); +// remote.read_with(cx, |remote, _| { +// assert_eq!( +// remote +// .paths() +// .map(|p| p.to_str().unwrap()) +// .collect::>(), +// expected_paths +// ); +// }); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_buffer_identity_across_renames( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a": { +// "file1": "", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs, [Path::new("/dir")], cx).await; +// let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); +// let tree_id = tree.read_with(cx, |tree, _| tree.id()); + +// let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { +// project.read_with(cx, |project, cx| { +// let tree = project.worktrees(cx).next().unwrap(); +// tree.read(cx) +// .entry_for_path(path) +// .unwrap_or_else(|| panic!("no entry for path {}", path)) +// .id +// }) +// }; + +// let dir_id = id_for_path("a", cx); +// let file_id = id_for_path("a/file1", cx); +// let buffer = project +// .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx)) +// .await +// .unwrap(); +// buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty())); + +// project +// .update(cx, |project, cx| { +// project.rename_entry(dir_id, Path::new("b"), cx) +// }) +// .unwrap() +// .await +// .unwrap(); +// deterministic.run_until_parked(); +// assert_eq!(id_for_path("b", cx), dir_id); +// assert_eq!(id_for_path("b/file1", cx), file_id); +// buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty())); +// } + +// #[gpui2::test] +// async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "a.txt": "a-contents", +// "b.txt": "b-contents", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// // Spawn multiple tasks to open paths, repeating some paths. +// let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { +// ( +// p.open_local_buffer("/dir/a.txt", cx), +// p.open_local_buffer("/dir/b.txt", cx), +// p.open_local_buffer("/dir/a.txt", cx), +// ) +// }); + +// let buffer_a_1 = buffer_a_1.await.unwrap(); +// let buffer_a_2 = buffer_a_2.await.unwrap(); +// let buffer_b = buffer_b.await.unwrap(); +// assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents"); +// assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents"); + +// // There is only one buffer per path. +// let buffer_a_id = buffer_a_1.id(); +// assert_eq!(buffer_a_2.id(), buffer_a_id); + +// // Open the same path again while it is still open. +// drop(buffer_a_1); +// let buffer_a_3 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx)) +// .await +// .unwrap(); + +// // There's still only one buffer per path. +// assert_eq!(buffer_a_3.id(), buffer_a_id); +// } + +// #[gpui2::test] +// async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "file1": "abc", +// "file2": "def", +// "file3": "ghi", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// let buffer1 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) +// .await +// .unwrap(); +// let events = Rc::new(RefCell::new(Vec::new())); + +// // initially, the buffer isn't dirty. +// buffer1.update(cx, |buffer, cx| { +// cx.subscribe(&buffer1, { +// let events = events.clone(); +// move |_, _, event, _| match event { +// BufferEvent::Operation(_) => {} +// _ => events.borrow_mut().push(event.clone()), +// } +// }) +// .detach(); + +// assert!(!buffer.is_dirty()); +// assert!(events.borrow().is_empty()); + +// buffer.edit([(1..2, "")], None, cx); +// }); + +// // after the first edit, the buffer is dirty, and emits a dirtied event. +// buffer1.update(cx, |buffer, cx| { +// assert!(buffer.text() == "ac"); +// assert!(buffer.is_dirty()); +// assert_eq!( +// *events.borrow(), +// &[language2::Event::Edited, language2::Event::DirtyChanged] +// ); +// events.borrow_mut().clear(); +// buffer.did_save( +// buffer.version(), +// buffer.as_rope().fingerprint(), +// buffer.file().unwrap().mtime(), +// cx, +// ); +// }); + +// // after saving, the buffer is not dirty, and emits a saved event. +// buffer1.update(cx, |buffer, cx| { +// assert!(!buffer.is_dirty()); +// assert_eq!(*events.borrow(), &[language2::Event::Saved]); +// events.borrow_mut().clear(); + +// buffer.edit([(1..1, "B")], None, cx); +// buffer.edit([(2..2, "D")], None, cx); +// }); + +// // after editing again, the buffer is dirty, and emits another dirty event. +// buffer1.update(cx, |buffer, cx| { +// assert!(buffer.text() == "aBDc"); +// assert!(buffer.is_dirty()); +// assert_eq!( +// *events.borrow(), +// &[ +// language2::Event::Edited, +// language2::Event::DirtyChanged, +// language2::Event::Edited, +// ], +// ); +// events.borrow_mut().clear(); + +// // After restoring the buffer to its previously-saved state, +// // the buffer is not considered dirty anymore. +// buffer.edit([(1..3, "")], None, cx); +// assert!(buffer.text() == "ac"); +// assert!(!buffer.is_dirty()); +// }); + +// assert_eq!( +// *events.borrow(), +// &[language2::Event::Edited, language2::Event::DirtyChanged] +// ); + +// // When a file is deleted, the buffer is considered dirty. +// let events = Rc::new(RefCell::new(Vec::new())); +// let buffer2 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) +// .await +// .unwrap(); +// buffer2.update(cx, |_, cx| { +// cx.subscribe(&buffer2, { +// let events = events.clone(); +// move |_, _, event, _| events.borrow_mut().push(event.clone()) +// }) +// .detach(); +// }); + +// fs.remove_file("/dir/file2".as_ref(), Default::default()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty())); +// assert_eq!( +// *events.borrow(), +// &[ +// language2::Event::DirtyChanged, +// language2::Event::FileHandleChanged +// ] +// ); + +// // When a file is already dirty when deleted, we don't emit a Dirtied event. +// let events = Rc::new(RefCell::new(Vec::new())); +// let buffer3 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx)) +// .await +// .unwrap(); +// buffer3.update(cx, |_, cx| { +// cx.subscribe(&buffer3, { +// let events = events.clone(); +// move |_, _, event, _| events.borrow_mut().push(event.clone()) +// }) +// .detach(); +// }); + +// buffer3.update(cx, |buffer, cx| { +// buffer.edit([(0..0, "x")], None, cx); +// }); +// events.borrow_mut().clear(); +// fs.remove_file("/dir/file3".as_ref(), Default::default()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// assert_eq!(*events.borrow(), &[language2::Event::FileHandleChanged]); +// cx.read(|cx| assert!(buffer3.read(cx).is_dirty())); +// } + +// #[gpui::test] +// async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let initial_contents = "aaa\nbbbbb\nc\n"; +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "the-file": initial_contents, +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx)) +// .await +// .unwrap(); + +// let anchors = (0..3) +// .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1)))) +// .collect::>(); + +// // Change the file on disk, adding two new lines of text, and removing +// // one line. +// buffer.read_with(cx, |buffer, _| { +// assert!(!buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// }); +// let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; +// fs.save( +// "/dir/the-file".as_ref(), +// &new_contents.into(), +// LineEnding::Unix, +// ) +// .await +// .unwrap(); + +// // Because the buffer was not modified, it is reloaded from disk. Its +// // contents are edited according to the diff between the old and new +// // file contents. +// cx.foreground().run_until_parked(); +// buffer.update(cx, |buffer, _| { +// assert_eq!(buffer.text(), new_contents); +// assert!(!buffer.is_dirty()); +// assert!(!buffer.has_conflict()); + +// let anchor_positions = anchors +// .iter() +// .map(|anchor| anchor.to_point(&*buffer)) +// .collect::>(); +// assert_eq!( +// anchor_positions, +// [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)] +// ); +// }); + +// // Modify the buffer +// buffer.update(cx, |buffer, cx| { +// buffer.edit([(0..0, " ")], None, cx); +// assert!(buffer.is_dirty()); +// assert!(!buffer.has_conflict()); +// }); + +// // Change the file on disk again, adding blank lines to the beginning. +// fs.save( +// "/dir/the-file".as_ref(), +// &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(), +// LineEnding::Unix, +// ) +// .await +// .unwrap(); + +// // Because the buffer is modified, it doesn't reload from disk, but is +// // marked as having a conflict. +// cx.foreground().run_until_parked(); +// buffer.read_with(cx, |buffer, _| { +// assert!(buffer.has_conflict()); +// }); +// } + +// #[gpui::test] +// async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "file1": "a\nb\nc\n", +// "file2": "one\r\ntwo\r\nthree\r\n", +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// let buffer1 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) +// .await +// .unwrap(); +// let buffer2 = project +// .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx)) +// .await +// .unwrap(); + +// buffer1.read_with(cx, |buffer, _| { +// assert_eq!(buffer.text(), "a\nb\nc\n"); +// assert_eq!(buffer.line_ending(), LineEnding::Unix); +// }); +// buffer2.read_with(cx, |buffer, _| { +// assert_eq!(buffer.text(), "one\ntwo\nthree\n"); +// assert_eq!(buffer.line_ending(), LineEnding::Windows); +// }); + +// // Change a file's line endings on disk from unix to windows. The buffer's +// // state updates correctly. +// fs.save( +// "/dir/file1".as_ref(), +// &"aaa\nb\nc\n".into(), +// LineEnding::Windows, +// ) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// buffer1.read_with(cx, |buffer, _| { +// assert_eq!(buffer.text(), "aaa\nb\nc\n"); +// assert_eq!(buffer.line_ending(), LineEnding::Windows); +// }); + +// // Save a file with windows line endings. The file is written correctly. +// buffer2.update(cx, |buffer, cx| { +// buffer.set_text("one\ntwo\nthree\nfour\n", cx); +// }); +// project +// .update(cx, |project, cx| project.save_buffer(buffer2, cx)) +// .await +// .unwrap(); +// assert_eq!( +// fs.load("/dir/file2".as_ref()).await.unwrap(), +// "one\r\ntwo\r\nthree\r\nfour\r\n", +// ); +// } + +// #[gpui::test] +// async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/the-dir", +// json!({ +// "a.rs": " +// fn foo(mut v: Vec) { +// for x in &v { +// v.push(1); +// } +// } +// " +// .unindent(), +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await; +// let buffer = project +// .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx)) +// .await +// .unwrap(); + +// let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap(); +// let message = lsp::PublishDiagnosticsParams { +// uri: buffer_uri.clone(), +// diagnostics: vec![ +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)), +// severity: Some(DiagnosticSeverity::WARNING), +// message: "error 1".to_string(), +// related_information: Some(vec![lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri.clone(), +// range: lsp2::Range::new( +// lsp2::Position::new(1, 8), +// lsp2::Position::new(1, 9), +// ), +// }, +// message: "error 1 hint 1".to_string(), +// }]), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)), +// severity: Some(DiagnosticSeverity::HINT), +// message: "error 1 hint 1".to_string(), +// related_information: Some(vec![lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri.clone(), +// range: lsp2::Range::new( +// lsp2::Position::new(1, 8), +// lsp2::Position::new(1, 9), +// ), +// }, +// message: "original diagnostic".to_string(), +// }]), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)), +// severity: Some(DiagnosticSeverity::ERROR), +// message: "error 2".to_string(), +// related_information: Some(vec![ +// lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri.clone(), +// range: lsp2::Range::new( +// lsp2::Position::new(1, 13), +// lsp2::Position::new(1, 15), +// ), +// }, +// message: "error 2 hint 1".to_string(), +// }, +// lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri.clone(), +// range: lsp2::Range::new( +// lsp2::Position::new(1, 13), +// lsp2::Position::new(1, 15), +// ), +// }, +// message: "error 2 hint 2".to_string(), +// }, +// ]), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)), +// severity: Some(DiagnosticSeverity::HINT), +// message: "error 2 hint 1".to_string(), +// related_information: Some(vec![lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri.clone(), +// range: lsp2::Range::new( +// lsp2::Position::new(2, 8), +// lsp2::Position::new(2, 17), +// ), +// }, +// message: "original diagnostic".to_string(), +// }]), +// ..Default::default() +// }, +// lsp2::Diagnostic { +// range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)), +// severity: Some(DiagnosticSeverity::HINT), +// message: "error 2 hint 2".to_string(), +// related_information: Some(vec![lsp::DiagnosticRelatedInformation { +// location: lsp::Location { +// uri: buffer_uri, +// range: lsp2::Range::new( +// lsp2::Position::new(2, 8), +// lsp2::Position::new(2, 17), +// ), +// }, +// message: "original diagnostic".to_string(), +// }]), +// ..Default::default() +// }, +// ], +// version: None, +// }; + +// project +// .update(cx, |p, cx| { +// p.update_diagnostics(LanguageServerId(0), message, &[], cx) +// }) +// .unwrap(); +// let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + +// assert_eq!( +// buffer +// .diagnostics_in_range::<_, Point>(0..buffer.len(), false) +// .collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(1, 8)..Point::new(1, 9), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::WARNING, +// message: "error 1".to_string(), +// group_id: 1, +// is_primary: true, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(1, 8)..Point::new(1, 9), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 1 hint 1".to_string(), +// group_id: 1, +// is_primary: false, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(1, 13)..Point::new(1, 15), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 2 hint 1".to_string(), +// group_id: 0, +// is_primary: false, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(1, 13)..Point::new(1, 15), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 2 hint 2".to_string(), +// group_id: 0, +// is_primary: false, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(2, 8)..Point::new(2, 17), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// message: "error 2".to_string(), +// group_id: 0, +// is_primary: true, +// ..Default::default() +// } +// } +// ] +// ); + +// assert_eq!( +// buffer.diagnostic_group::(0).collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(1, 13)..Point::new(1, 15), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 2 hint 1".to_string(), +// group_id: 0, +// is_primary: false, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(1, 13)..Point::new(1, 15), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 2 hint 2".to_string(), +// group_id: 0, +// is_primary: false, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(2, 8)..Point::new(2, 17), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::ERROR, +// message: "error 2".to_string(), +// group_id: 0, +// is_primary: true, +// ..Default::default() +// } +// } +// ] +// ); + +// assert_eq!( +// buffer.diagnostic_group::(1).collect::>(), +// &[ +// DiagnosticEntry { +// range: Point::new(1, 8)..Point::new(1, 9), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::WARNING, +// message: "error 1".to_string(), +// group_id: 1, +// is_primary: true, +// ..Default::default() +// } +// }, +// DiagnosticEntry { +// range: Point::new(1, 8)..Point::new(1, 9), +// diagnostic: Diagnostic { +// severity: DiagnosticSeverity::HINT, +// message: "error 1 hint 1".to_string(), +// group_id: 1, +// is_primary: false, +// ..Default::default() +// } +// }, +// ] +// ); +// } + +// #[gpui::test] +// async fn test_rename(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let mut language = Language::new( +// LanguageConfig { +// name: "Rust".into(), +// path_suffixes: vec!["rs".to_string()], +// ..Default::default() +// }, +// Some(tree_sitter_rust::language()), +// ); +// let mut fake_servers = language +// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { +// capabilities: lsp2::ServerCapabilities { +// rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions { +// prepare_provider: Some(true), +// work_done_progress_options: Default::default(), +// })), +// ..Default::default() +// }, +// ..Default::default() +// })) +// .await; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;" +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// project.update(cx, |project, _| project.languages.add(Arc::new(language))); +// let buffer = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/dir/one.rs", cx) +// }) +// .await +// .unwrap(); + +// let fake_server = fake_servers.next().await.unwrap(); + +// let response = project.update(cx, |project, cx| { +// project.prepare_rename(buffer.clone(), 7, cx) +// }); +// fake_server +// .handle_request::(|params, _| async move { +// assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); +// assert_eq!(params.position, lsp2::Position::new(0, 7)); +// Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new( +// lsp2::Position::new(0, 6), +// lsp2::Position::new(0, 9), +// )))) +// }) +// .next() +// .await +// .unwrap(); +// let range = response.await.unwrap().unwrap(); +// let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer)); +// assert_eq!(range, 6..9); + +// let response = project.update(cx, |project, cx| { +// project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) +// }); +// fake_server +// .handle_request::(|params, _| async move { +// assert_eq!( +// params.text_document_position.text_document.uri.as_str(), +// "file:///dir/one.rs" +// ); +// assert_eq!( +// params.text_document_position.position, +// lsp2::Position::new(0, 7) +// ); +// assert_eq!(params.new_name, "THREE"); +// Ok(Some(lsp::WorkspaceEdit { +// changes: Some( +// [ +// ( +// lsp2::Url::from_file_path("/dir/one.rs").unwrap(), +// vec![lsp2::TextEdit::new( +// lsp2::Range::new( +// lsp2::Position::new(0, 6), +// lsp2::Position::new(0, 9), +// ), +// "THREE".to_string(), +// )], +// ), +// ( +// lsp2::Url::from_file_path("/dir/two.rs").unwrap(), +// vec![ +// lsp2::TextEdit::new( +// lsp2::Range::new( +// lsp2::Position::new(0, 24), +// lsp2::Position::new(0, 27), +// ), +// "THREE".to_string(), +// ), +// lsp2::TextEdit::new( +// lsp2::Range::new( +// lsp2::Position::new(0, 35), +// lsp2::Position::new(0, 38), +// ), +// "THREE".to_string(), +// ), +// ], +// ), +// ] +// .into_iter() +// .collect(), +// ), +// ..Default::default() +// })) +// }) +// .next() +// .await +// .unwrap(); +// let mut transaction = response.await.unwrap().0; +// assert_eq!(transaction.len(), 2); +// assert_eq!( +// transaction +// .remove_entry(&buffer) +// .unwrap() +// .0 +// .read_with(cx, |buffer, _| buffer.text()), +// "const THREE: usize = 1;" +// ); +// assert_eq!( +// transaction +// .into_keys() +// .next() +// .unwrap() +// .read_with(cx, |buffer, _| buffer.text()), +// "const TWO: usize = one::THREE + one::THREE;" +// ); +// } + +// #[gpui::test] +// async fn test_search(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": "const ONE: usize = 1;", +// "two.rs": "const TWO: usize = one::ONE + one::ONE;", +// "three.rs": "const THREE: usize = one::ONE + two::TWO;", +// "four.rs": "const FOUR: usize = one::ONE + three::THREE;", +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; +// assert_eq!( +// search( +// &project, +// SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("two.rs".to_string(), vec![6..9]), +// ("three.rs".to_string(), vec![37..40]) +// ]) +// ); + +// let buffer_4 = project +// .update(cx, |project, cx| { +// project.open_local_buffer("/dir/four.rs", cx) +// }) +// .await +// .unwrap(); +// buffer_4.update(cx, |buffer, cx| { +// let text = "two::TWO"; +// buffer.edit([(20..28, text), (31..43, text)], None, cx); +// }); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("two.rs".to_string(), vec![6..9]), +// ("three.rs".to_string(), vec![37..40]), +// ("four.rs".to_string(), vec![25..28, 36..39]) +// ]) +// ); +// } + +// #[gpui::test] +// async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let search_query = "file"; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": r#"// Rust file one"#, +// "one.ts": r#"// TypeScript file one"#, +// "two.rs": r#"// Rust file two"#, +// "two.ts": r#"// TypeScript file two"#, +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// assert!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![PathMatcher::new("*.odd").unwrap()], +// Vec::new() +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap() +// .is_empty(), +// "If no inclusions match, no files should be returned" +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![PathMatcher::new("*.rs").unwrap()], +// Vec::new() +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.rs".to_string(), vec![8..12]), +// ("two.rs".to_string(), vec![8..12]), +// ]), +// "Rust only search should give only Rust files" +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![ +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap(), +// ], +// Vec::new() +// ).unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.ts".to_string(), vec![14..18]), +// ("two.ts".to_string(), vec![14..18]), +// ]), +// "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything" +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![ +// PathMatcher::new("*.rs").unwrap(), +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap(), +// ], +// Vec::new() +// ).unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.rs".to_string(), vec![8..12]), +// ("one.ts".to_string(), vec![14..18]), +// ("two.rs".to_string(), vec![8..12]), +// ("two.ts".to_string(), vec![14..18]), +// ]), +// "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything" +// ); +// } + +// #[gpui::test] +// async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let search_query = "file"; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": r#"// Rust file one"#, +// "one.ts": r#"// TypeScript file one"#, +// "two.rs": r#"// Rust file two"#, +// "two.ts": r#"// TypeScript file two"#, +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// Vec::new(), +// vec![PathMatcher::new("*.odd").unwrap()], +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.rs".to_string(), vec![8..12]), +// ("one.ts".to_string(), vec![14..18]), +// ("two.rs".to_string(), vec![8..12]), +// ("two.ts".to_string(), vec![14..18]), +// ]), +// "If no exclusions match, all files should be returned" +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// Vec::new(), +// vec![PathMatcher::new("*.rs").unwrap()], +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.ts".to_string(), vec![14..18]), +// ("two.ts".to_string(), vec![14..18]), +// ]), +// "Rust exclusion search should give only TypeScript files" +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// Vec::new(), +// vec![ +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap(), +// ], +// ).unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.rs".to_string(), vec![8..12]), +// ("two.rs".to_string(), vec![8..12]), +// ]), +// "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything" +// ); + +// assert!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// Vec::new(), +// vec![ +// PathMatcher::new("*.rs").unwrap(), +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap(), +// ], +// ).unwrap(), +// cx +// ) +// .await +// .unwrap().is_empty(), +// "Rust and typescript exclusion should give no files, even if other exclusions don't match anything" +// ); +// } + +// #[gpui::test] +// async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let search_query = "file"; + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/dir", +// json!({ +// "one.rs": r#"// Rust file one"#, +// "one.ts": r#"// TypeScript file one"#, +// "two.rs": r#"// Rust file two"#, +// "two.ts": r#"// TypeScript file two"#, +// }), +// ) +// .await; +// let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + +// assert!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![PathMatcher::new("*.odd").unwrap()], +// vec![PathMatcher::new("*.odd").unwrap()], +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap() +// .is_empty(), +// "If both no exclusions and inclusions match, exclusions should win and return nothing" +// ); + +// assert!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![PathMatcher::new("*.ts").unwrap()], +// vec![PathMatcher::new("*.ts").unwrap()], +// ).unwrap(), +// cx +// ) +// .await +// .unwrap() +// .is_empty(), +// "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files." +// ); + +// assert!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![ +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap() +// ], +// vec![ +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap() +// ], +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap() +// .is_empty(), +// "Non-matching inclusions and exclusions should not change that." +// ); + +// assert_eq!( +// search( +// &project, +// SearchQuery::text( +// search_query, +// false, +// true, +// vec![ +// PathMatcher::new("*.ts").unwrap(), +// PathMatcher::new("*.odd").unwrap() +// ], +// vec![ +// PathMatcher::new("*.rs").unwrap(), +// PathMatcher::new("*.odd").unwrap() +// ], +// ) +// .unwrap(), +// cx +// ) +// .await +// .unwrap(), +// HashMap::from_iter([ +// ("one.ts".to_string(), vec![14..18]), +// ("two.ts".to_string(), vec![14..18]), +// ]), +// "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files" +// ); +// } + +// #[test] +// fn test_glob_literal_prefix() { +// assert_eq!(glob_literal_prefix("**/*.js"), ""); +// assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules"); +// assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo"); +// assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); +// } + +// async fn search( +// project: &ModelHandle, +// query: SearchQuery, +// cx: &mut gpui::TestAppContext, +// ) -> Result>>> { +// let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); +// let mut result = HashMap::default(); +// while let Some((buffer, range)) = search_rx.next().await { +// result.entry(buffer).or_insert(range); +// } +// Ok(result +// .into_iter() +// .map(|(buffer, ranges)| { +// buffer.read_with(cx, |buffer, _| { +// let path = buffer.file().unwrap().path().to_string_lossy().to_string(); +// let ranges = ranges +// .into_iter() +// .map(|range| range.to_offset(buffer)) +// .collect::>(); +// (path, ranges) +// }) +// }) +// .collect()) +// } + +// fn init_test(cx: &mut gpui::TestAppContext) { +// cx.foreground().forbid_parking(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// language2::init(cx); +// Project::init_settings(cx); +// }); +// } diff --git a/crates/project2/src/search.rs b/crates/project2/src/search.rs new file mode 100644 index 0000000000..9af13c7732 --- /dev/null +++ b/crates/project2/src/search.rs @@ -0,0 +1,458 @@ +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; +use anyhow::{Context, Result}; +use client2::proto; +use globset::{Glob, GlobMatcher}; +use itertools::Itertools; +use language2::{char_kind, BufferSnapshot}; +use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; +use std::{ + borrow::Cow, + io::{BufRead, BufReader, Read}, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, +}; + +#[derive(Clone, Debug)] +pub struct SearchInputs { + query: Arc, + files_to_include: Vec, + files_to_exclude: Vec, +} + +impl SearchInputs { + pub fn as_str(&self) -> &str { + self.query.as_ref() + } + pub fn files_to_include(&self) -> &[PathMatcher] { + &self.files_to_include + } + pub fn files_to_exclude(&self) -> &[PathMatcher] { + &self.files_to_exclude + } +} +#[derive(Clone, Debug)] +pub enum SearchQuery { + Text { + search: Arc, + replacement: Option, + whole_word: bool, + case_sensitive: bool, + inner: SearchInputs, + }, + + Regex { + regex: Regex, + replacement: Option, + multiline: bool, + whole_word: bool, + case_sensitive: bool, + inner: SearchInputs, + }, +} + +#[derive(Clone, Debug)] +pub struct PathMatcher { + maybe_path: PathBuf, + glob: GlobMatcher, +} + +impl std::fmt::Display for PathMatcher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.maybe_path.to_string_lossy().fmt(f) + } +} + +impl PathMatcher { + pub fn new(maybe_glob: &str) -> Result { + Ok(PathMatcher { + glob: Glob::new(&maybe_glob)?.compile_matcher(), + maybe_path: PathBuf::from(maybe_glob), + }) + } + + pub fn is_match>(&self, other: P) -> bool { + other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other) + } +} + +impl SearchQuery { + pub fn text( + query: impl ToString, + whole_word: bool, + case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, + ) -> Result { + let query = query.to_string(); + let search = AhoCorasickBuilder::new() + .ascii_case_insensitive(!case_sensitive) + .build(&[&query])?; + let inner = SearchInputs { + query: query.into(), + files_to_exclude, + files_to_include, + }; + Ok(Self::Text { + search: Arc::new(search), + replacement: None, + whole_word, + case_sensitive, + inner, + }) + } + + pub fn regex( + query: impl ToString, + whole_word: bool, + case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, + ) -> Result { + let mut query = query.to_string(); + let initial_query = Arc::from(query.as_str()); + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query + } + + let multiline = query.contains('\n') || query.contains("\\n"); + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(multiline) + .build()?; + let inner = SearchInputs { + query: initial_query, + files_to_exclude, + files_to_include, + }; + Ok(Self::Regex { + regex, + replacement: None, + multiline, + whole_word, + case_sensitive, + inner, + }) + } + + pub fn from_proto(message: proto::SearchProject) -> Result { + if message.regex { + Self::regex( + message.query, + message.whole_word, + message.case_sensitive, + deserialize_path_matches(&message.files_to_include)?, + deserialize_path_matches(&message.files_to_exclude)?, + ) + } else { + Self::text( + message.query, + message.whole_word, + message.case_sensitive, + deserialize_path_matches(&message.files_to_include)?, + deserialize_path_matches(&message.files_to_exclude)?, + ) + } + } + pub fn with_replacement(mut self, new_replacement: String) -> Self { + match self { + Self::Text { + ref mut replacement, + .. + } + | Self::Regex { + ref mut replacement, + .. + } => { + *replacement = Some(new_replacement); + self + } + } + } + pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { + proto::SearchProject { + project_id, + query: self.as_str().to_string(), + regex: self.is_regex(), + whole_word: self.whole_word(), + case_sensitive: self.case_sensitive(), + files_to_include: self + .files_to_include() + .iter() + .map(|matcher| matcher.to_string()) + .join(","), + files_to_exclude: self + .files_to_exclude() + .iter() + .map(|matcher| matcher.to_string()) + .join(","), + } + } + + pub fn detect(&self, stream: T) -> Result { + if self.as_str().is_empty() { + return Ok(false); + } + + match self { + Self::Text { search, .. } => { + let mat = search.stream_find_iter(stream).next(); + match mat { + Some(Ok(_)) => Ok(true), + Some(Err(err)) => Err(err.into()), + None => Ok(false), + } + } + Self::Regex { + regex, multiline, .. + } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + if let Err(err) = reader.read_to_string(&mut text) { + Err(err.into()) + } else { + Ok(regex.find(&text).is_some()) + } + } else { + for line in reader.lines() { + let line = line?; + if regex.find(&line).is_some() { + return Ok(true); + } + } + Ok(false) + } + } + } + } + /// Returns the replacement text for this `SearchQuery`. + pub fn replacement(&self) -> Option<&str> { + match self { + SearchQuery::Text { replacement, .. } | SearchQuery::Regex { replacement, .. } => { + replacement.as_deref() + } + } + } + /// Replaces search hits if replacement is set. `text` is assumed to be a string that matches this `SearchQuery` exactly, without any leftovers on either side. + pub fn replacement_for<'a>(&self, text: &'a str) -> Option> { + match self { + SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from), + SearchQuery::Regex { + regex, replacement, .. + } => { + if let Some(replacement) = replacement { + Some(regex.replace(text, replacement)) + } else { + None + } + } + } + } + pub async fn search( + &self, + buffer: &BufferSnapshot, + subrange: Option>, + ) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + + if self.as_str().is_empty() { + return Default::default(); + } + + let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0); + let rope = if let Some(range) = subrange { + buffer.as_rope().slice(range) + } else { + buffer.as_rope().clone() + }; + + let mut matches = Vec::new(); + match self { + Self::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let scope = buffer.language_scope_at(range_offset + mat.start()); + let kind = |c| char_kind(&scope, c); + + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); + let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + matches.push(mat.start()..mat.end()) + } + } + + Self::Regex { + regex, multiline, .. + } => { + if *multiline { + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } + } else { + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + } + } + + matches + } + + pub fn as_str(&self) -> &str { + self.as_inner().as_str() + } + + pub fn whole_word(&self) -> bool { + match self { + Self::Text { whole_word, .. } => *whole_word, + Self::Regex { whole_word, .. } => *whole_word, + } + } + + pub fn case_sensitive(&self) -> bool { + match self { + Self::Text { case_sensitive, .. } => *case_sensitive, + Self::Regex { case_sensitive, .. } => *case_sensitive, + } + } + + pub fn is_regex(&self) -> bool { + matches!(self, Self::Regex { .. }) + } + + pub fn files_to_include(&self) -> &[PathMatcher] { + self.as_inner().files_to_include() + } + + pub fn files_to_exclude(&self) -> &[PathMatcher] { + self.as_inner().files_to_exclude() + } + + pub fn file_matches(&self, file_path: Option<&Path>) -> bool { + match file_path { + Some(file_path) => { + !self + .files_to_exclude() + .iter() + .any(|exclude_glob| exclude_glob.is_match(file_path)) + && (self.files_to_include().is_empty() + || self + .files_to_include() + .iter() + .any(|include_glob| include_glob.is_match(file_path))) + } + None => self.files_to_include().is_empty(), + } + } + pub fn as_inner(&self) -> &SearchInputs { + match self { + Self::Regex { inner, .. } | Self::Text { inner, .. } => inner, + } + } +} + +fn deserialize_path_matches(glob_set: &str) -> anyhow::Result> { + glob_set + .split(',') + .map(str::trim) + .filter(|glob_str| !glob_str.is_empty()) + .map(|glob_str| { + PathMatcher::new(glob_str) + .with_context(|| format!("deserializing path match glob {glob_str}")) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_matcher_creation_for_valid_paths() { + for valid_path in [ + "file", + "Cargo.toml", + ".DS_Store", + "~/dir/another_dir/", + "./dir/file", + "dir/[a-z].txt", + "../dir/filé", + ] { + let path_matcher = PathMatcher::new(valid_path).unwrap_or_else(|e| { + panic!("Valid path {valid_path} should be accepted, but got: {e}") + }); + assert!( + path_matcher.is_match(valid_path), + "Path matcher for valid path {valid_path} should match itself" + ) + } + } + + #[test] + fn path_matcher_creation_for_globs() { + for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] { + match PathMatcher::new(invalid_glob) { + Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"), + Err(_expected) => {} + } + } + + for valid_glob in [ + "dir/?ile", + "dir/*.txt", + "dir/**/file", + "dir/[a-z].txt", + "{dir,file}", + ] { + match PathMatcher::new(valid_glob) { + Ok(_expected) => {} + Err(e) => panic!("Valid glob {valid_glob} should be accepted, but got: {e}"), + } + } + } +} diff --git a/crates/project2/src/terminals.rs b/crates/project2/src/terminals.rs new file mode 100644 index 0000000000..a47fb39105 --- /dev/null +++ b/crates/project2/src/terminals.rs @@ -0,0 +1,124 @@ +use crate::Project; +use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; +use std::path::{Path, PathBuf}; +use terminal::{ + terminal_settings::{self, TerminalSettings, VenvSettingsContent}, + Terminal, TerminalBuilder, +}; + +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; + +pub struct Terminals { + pub(crate) local_handles: Vec>, +} + +impl Project { + pub fn create_terminal( + &mut self, + working_directory: Option, + window: AnyWindowHandle, + cx: &mut ModelContext, + ) -> anyhow::Result> { + if self.is_remote() { + return Err(anyhow::anyhow!( + "creating terminals as a guest is not supported yet" + )); + } else { + let settings = settings::get::(cx); + let python_settings = settings.detect_venv.clone(); + let shell = settings.shell.clone(); + + let terminal = TerminalBuilder::new( + working_directory.clone(), + shell.clone(), + settings.env.clone(), + Some(settings.blinking.clone()), + settings.alternate_scroll, + window, + ) + .map(|builder| { + let terminal_handle = cx.add_model(|cx| builder.subscribe(cx)); + + self.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles.iter().position(|terminal| terminal.id() == id) { + handles.remove(index); + cx.notify(); + } + }) + .detach(); + + if let Some(python_settings) = &python_settings.as_option() { + let activate_script_path = + self.find_activate_script_path(&python_settings, working_directory); + self.activate_python_virtual_environment( + activate_script_path, + &terminal_handle, + cx, + ); + } + terminal_handle + }); + + terminal + } + } + + pub fn find_activate_script_path( + &mut self, + settings: &VenvSettingsContent, + working_directory: Option, + ) -> Option { + // When we are unable to resolve the working directory, the terminal builder + // defaults to '/'. We should probably encode this directly somewhere, but for + // now, let's just hard code it here. + let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf()); + let activate_script_name = match settings.activate_script { + terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Csh => "activate.csh", + terminal_settings::ActivateScript::Fish => "activate.fish", + terminal_settings::ActivateScript::Nushell => "activate.nu", + }; + + for virtual_environment_name in settings.directories { + let mut path = working_directory.join(virtual_environment_name); + path.push("bin/"); + path.push(activate_script_name); + + if path.exists() { + return Some(path); + } + } + + None + } + + fn activate_python_virtual_environment( + &mut self, + activate_script: Option, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(activate_script) = activate_script { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + + pub fn local_terminal_handles(&self) -> &Vec> { + &self.terminals.local_handles + } +} + +// TODO: Add a few tests for adding and removing terminal tabs diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs new file mode 100644 index 0000000000..8842ce42de --- /dev/null +++ b/crates/project2/src/worktree.rs @@ -0,0 +1,4387 @@ +use crate::{ + copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions, +}; +use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; +use anyhow::{anyhow, Context, Result}; +use client2::{proto, Client}; +use clock::ReplicaId; +use collections::{HashMap, HashSet, VecDeque}; +use fs::{ + repository::{GitFileStatus, GitRepository, RepoPath}, + Fs, +}; +use futures::{ + channel::{ + mpsc::{self, UnboundedSender}, + oneshot, + }, + select_biased, + task::Poll, + FutureExt, Stream, StreamExt, +}; +use fuzzy2::CharBag; +use git::{DOT_GIT, GITIGNORE}; +use gpui2::{AppContext, AsyncAppContext, EventEmitter, Executor, Handle, ModelContext, Task}; +use language2::{ + proto::{ + deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, + serialize_version, + }, + Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, +}; +use lsp2::LanguageServerId; +use parking_lot::Mutex; +use postage::{ + barrier, + prelude::{Sink as _, Stream as _}, + watch, +}; +use smol::channel::{self, Sender}; +use std::{ + any::Any, + cmp::{self, Ordering}, + convert::TryFrom, + ffi::OsStr, + fmt, + future::Future, + mem, + ops::{AddAssign, Deref, DerefMut, Sub}, + path::{Path, PathBuf}, + pin::Pin, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, + time::{Duration, SystemTime}, +}; +use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; +use util::{paths::HOME, ResultExt}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] +pub struct WorktreeId(usize); + +pub enum Worktree { + Local(LocalWorktree), + Remote(RemoteWorktree), +} + +pub struct LocalWorktree { + snapshot: LocalSnapshot, + scan_requests_tx: channel::Sender, + path_prefixes_to_scan_tx: channel::Sender>, + is_scanning: (watch::Sender, watch::Receiver), + _background_scanner_task: Task<()>, + share: Option, + diagnostics: HashMap< + Arc, + Vec<( + LanguageServerId, + Vec>>, + )>, + >, + diagnostic_summaries: HashMap, HashMap>, + client: Arc, + fs: Arc, + visible: bool, +} + +struct ScanRequest { + relative_paths: Vec>, + done: barrier::Sender, +} + +pub struct RemoteWorktree { + snapshot: Snapshot, + background_snapshot: Arc>, + project_id: u64, + client: Arc, + updates_tx: Option>, + snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, + replica_id: ReplicaId, + diagnostic_summaries: HashMap, HashMap>, + visible: bool, + disconnected: bool, +} + +#[derive(Clone)] +pub struct Snapshot { + id: WorktreeId, + abs_path: Arc, + root_name: String, + root_char_bag: CharBag, + entries_by_path: SumTree, + entries_by_id: SumTree, + repository_entries: TreeMap, + + /// A number that increases every time the worktree begins scanning + /// a set of paths from the filesystem. This scanning could be caused + /// by some operation performed on the worktree, such as reading or + /// writing a file, or by an event reported by the filesystem. + scan_id: usize, + + /// The latest scan id that has completed, and whose preceding scans + /// have all completed. The current `scan_id` could be more than one + /// greater than the `completed_scan_id` if operations are performed + /// on the worktree while it is processing a file-system event. + completed_scan_id: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RepositoryEntry { + pub(crate) work_directory: WorkDirectoryEntry, + pub(crate) branch: Option>, +} + +impl RepositoryEntry { + pub fn branch(&self) -> Option> { + self.branch.clone() + } + + pub fn work_directory_id(&self) -> ProjectEntryId { + *self.work_directory + } + + pub fn work_directory(&self, snapshot: &Snapshot) -> Option { + snapshot + .entry_for_id(self.work_directory_id()) + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + } + + pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { + proto::RepositoryEntry { + work_directory_id: self.work_directory_id().to_proto(), + branch: self.branch.as_ref().map(|str| str.to_string()), + } + } +} + +impl From<&RepositoryEntry> for proto::RepositoryEntry { + fn from(value: &RepositoryEntry) -> Self { + proto::RepositoryEntry { + work_directory_id: value.work_directory.to_proto(), + branch: value.branch.as_ref().map(|str| str.to_string()), + } + } +} + +/// This path corresponds to the 'content path' (the folder that contains the .git) +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct RepositoryWorkDirectory(pub(crate) Arc); + +impl Default for RepositoryWorkDirectory { + fn default() -> Self { + RepositoryWorkDirectory(Arc::from(Path::new(""))) + } +} + +impl AsRef for RepositoryWorkDirectory { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub struct WorkDirectoryEntry(ProjectEntryId); + +impl WorkDirectoryEntry { + pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option { + worktree.entry_for_id(self.0).and_then(|entry| { + path.strip_prefix(&entry.path) + .ok() + .map(move |path| path.into()) + }) + } +} + +impl Deref for WorkDirectoryEntry { + type Target = ProjectEntryId; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> From for WorkDirectoryEntry { + fn from(value: ProjectEntryId) -> Self { + WorkDirectoryEntry(value) + } +} + +#[derive(Debug, Clone)] +pub struct LocalSnapshot { + snapshot: Snapshot, + /// All of the gitignore files in the worktree, indexed by their relative path. + /// The boolean indicates whether the gitignore needs to be updated. + ignores_by_parent_abs_path: HashMap, (Arc, bool)>, + /// All of the git repositories in the worktree, indexed by the project entry + /// id of their parent directory. + git_repositories: TreeMap, +} + +struct BackgroundScannerState { + snapshot: LocalSnapshot, + scanned_dirs: HashSet, + path_prefixes_to_scan: HashSet>, + paths_to_scan: HashSet>, + /// The ids of all of the entries that were removed from the snapshot + /// as part of the current update. These entry ids may be re-used + /// if the same inode is discovered at a new path, or if the given + /// path is re-created after being deleted. + removed_entry_ids: HashMap, + changed_paths: Vec>, + prev_snapshot: Snapshot, +} + +#[derive(Debug, Clone)] +pub struct LocalRepositoryEntry { + pub(crate) git_dir_scan_id: usize, + pub(crate) repo_ptr: Arc>, + /// Path to the actual .git folder. + /// Note: if .git is a file, this points to the folder indicated by the .git file + pub(crate) git_dir_path: Arc, +} + +impl Deref for LocalSnapshot { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl DerefMut for LocalSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.snapshot + } +} + +enum ScanState { + Started, + Updated { + snapshot: LocalSnapshot, + changes: UpdatedEntriesSet, + barrier: Option, + scanning: bool, + }, +} + +struct ShareState { + project_id: u64, + snapshots_tx: + mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>, + resume_updates: watch::Sender<()>, + _maintain_remote_snapshot: Task>, +} + +pub enum Event { + UpdatedEntries(UpdatedEntriesSet), + UpdatedGitRepositories(UpdatedGitRepositoriesSet), +} + +impl EventEmitter for Worktree { + type Event = Event; +} + +impl Worktree { + pub async fn local( + client: Arc, + path: impl Into>, + visible: bool, + fs: Arc, + next_entry_id: Arc, + cx: &mut AsyncAppContext, + ) -> Result> { + // After determining whether the root entry is a file or a directory, populate the + // snapshot's "root name", which will be used for the purpose of fuzzy matching. + let abs_path = path.into(); + let metadata = fs + .metadata(&abs_path) + .await + .context("failed to stat worktree path")?; + + Ok(cx.add_model(move |cx: &mut ModelContext| { + let root_name = abs_path + .file_name() + .map_or(String::new(), |f| f.to_string_lossy().to_string()); + + let mut snapshot = LocalSnapshot { + ignores_by_parent_abs_path: Default::default(), + git_repositories: Default::default(), + snapshot: Snapshot { + id: WorktreeId::from_usize(cx.model_id()), + abs_path: abs_path.clone(), + root_name: root_name.clone(), + root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), + entries_by_path: Default::default(), + entries_by_id: Default::default(), + repository_entries: Default::default(), + scan_id: 1, + completed_scan_id: 0, + }, + }; + + if let Some(metadata) = metadata { + snapshot.insert_entry( + Entry::new( + Arc::from(Path::new("")), + &metadata, + &next_entry_id, + snapshot.root_char_bag, + ), + fs.as_ref(), + ); + } + + let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); + let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); + let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); + + cx.spawn_weak(|this, mut cx| async move { + while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) { + this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + match state { + ScanState::Started => { + *this.is_scanning.0.borrow_mut() = true; + } + ScanState::Updated { + snapshot, + changes, + barrier, + scanning, + } => { + *this.is_scanning.0.borrow_mut() = scanning; + this.set_snapshot(snapshot, changes, cx); + drop(barrier); + } + } + cx.notify(); + }); + } + }) + .detach(); + + let background_scanner_task = cx.background().spawn({ + let fs = fs.clone(); + let snapshot = snapshot.clone(); + let background = cx.background().clone(); + async move { + let events = fs.watch(&abs_path, Duration::from_millis(100)).await; + BackgroundScanner::new( + snapshot, + next_entry_id, + fs, + scan_states_tx, + background, + scan_requests_rx, + path_prefixes_to_scan_rx, + ) + .run(events) + .await; + } + }); + + Worktree::Local(LocalWorktree { + snapshot, + is_scanning: watch::channel_with(true), + share: None, + scan_requests_tx, + path_prefixes_to_scan_tx, + _background_scanner_task: background_scanner_task, + diagnostics: Default::default(), + diagnostic_summaries: Default::default(), + client, + fs, + visible, + }) + })) + } + + // abcdefghi + pub fn remote( + project_remote_id: u64, + replica_id: ReplicaId, + worktree: proto::WorktreeMetadata, + client: Arc, + cx: &mut AppContext, + ) -> Handle { + cx.add_model(|cx: &mut ModelContext| { + let snapshot = Snapshot { + id: WorktreeId(worktree.id as usize), + abs_path: Arc::from(PathBuf::from(worktree.abs_path)), + root_name: worktree.root_name.clone(), + root_char_bag: worktree + .root_name + .chars() + .map(|c| c.to_ascii_lowercase()) + .collect(), + entries_by_path: Default::default(), + entries_by_id: Default::default(), + repository_entries: Default::default(), + scan_id: 1, + completed_scan_id: 0, + }; + + let (updates_tx, mut updates_rx) = mpsc::unbounded(); + let background_snapshot = Arc::new(Mutex::new(snapshot.clone())); + let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); + + cx.background() + .spawn({ + let background_snapshot = background_snapshot.clone(); + async move { + while let Some(update) = updates_rx.next().await { + if let Err(error) = + background_snapshot.lock().apply_remote_update(update) + { + log::error!("error applying worktree update: {}", error); + } + snapshot_updated_tx.send(()).await.ok(); + } + } + }) + .detach(); + + cx.spawn_weak(|this, mut cx| async move { + while (snapshot_updated_rx.recv().await).is_some() { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + let this = this.as_remote_mut().unwrap(); + this.snapshot = this.background_snapshot.lock().clone(); + cx.emit(Event::UpdatedEntries(Arc::from([]))); + cx.notify(); + while let Some((scan_id, _)) = this.snapshot_subscriptions.front() { + if this.observed_snapshot(*scan_id) { + let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap(); + let _ = tx.send(()); + } else { + break; + } + } + }); + } else { + break; + } + } + }) + .detach(); + + Worktree::Remote(RemoteWorktree { + project_id: project_remote_id, + replica_id, + snapshot: snapshot.clone(), + background_snapshot, + updates_tx: Some(updates_tx), + snapshot_subscriptions: Default::default(), + client: client.clone(), + diagnostic_summaries: Default::default(), + visible: worktree.visible, + disconnected: false, + }) + }) + } + + pub fn as_local(&self) -> Option<&LocalWorktree> { + if let Worktree::Local(worktree) = self { + Some(worktree) + } else { + None + } + } + + pub fn as_remote(&self) -> Option<&RemoteWorktree> { + if let Worktree::Remote(worktree) = self { + Some(worktree) + } else { + None + } + } + + pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> { + if let Worktree::Local(worktree) = self { + Some(worktree) + } else { + None + } + } + + pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> { + if let Worktree::Remote(worktree) = self { + Some(worktree) + } else { + None + } + } + + pub fn is_local(&self) -> bool { + matches!(self, Worktree::Local(_)) + } + + pub fn is_remote(&self) -> bool { + !self.is_local() + } + + pub fn snapshot(&self) -> Snapshot { + match self { + Worktree::Local(worktree) => worktree.snapshot().snapshot, + Worktree::Remote(worktree) => worktree.snapshot(), + } + } + + pub fn scan_id(&self) -> usize { + match self { + Worktree::Local(worktree) => worktree.snapshot.scan_id, + Worktree::Remote(worktree) => worktree.snapshot.scan_id, + } + } + + pub fn completed_scan_id(&self) -> usize { + match self { + Worktree::Local(worktree) => worktree.snapshot.completed_scan_id, + Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id, + } + } + + pub fn is_visible(&self) -> bool { + match self { + Worktree::Local(worktree) => worktree.visible, + Worktree::Remote(worktree) => worktree.visible, + } + } + + pub fn replica_id(&self) -> ReplicaId { + match self { + Worktree::Local(_) => 0, + Worktree::Remote(worktree) => worktree.replica_id, + } + } + + pub fn diagnostic_summaries( + &self, + ) -> impl Iterator, LanguageServerId, DiagnosticSummary)> + '_ { + match self { + Worktree::Local(worktree) => &worktree.diagnostic_summaries, + Worktree::Remote(worktree) => &worktree.diagnostic_summaries, + } + .iter() + .flat_map(|(path, summaries)| { + summaries + .iter() + .map(move |(&server_id, &summary)| (path.clone(), server_id, summary)) + }) + } + + pub fn abs_path(&self) -> Arc { + match self { + Worktree::Local(worktree) => worktree.abs_path.clone(), + Worktree::Remote(worktree) => worktree.abs_path.clone(), + } + } + + pub fn root_file(&self, cx: &mut ModelContext) -> Option> { + let entry = self.root_entry()?; + Some(File::for_entry(entry.clone(), cx.handle())) + } +} + +impl LocalWorktree { + pub fn contains_abs_path(&self, path: &Path) -> bool { + path.starts_with(&self.abs_path) + } + + pub(crate) fn load_buffer( + &mut self, + id: u64, + path: &Path, + cx: &mut ModelContext, + ) -> Task>> { + let path = Arc::from(path); + cx.spawn(move |this, mut cx| async move { + let (file, contents, diff_base) = this + .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) + .await?; + let text_buffer = cx + .background() + .spawn(async move { text::Buffer::new(0, id, contents) }) + .await; + Ok(cx.add_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))) + }) + } + + pub fn diagnostics_for_path( + &self, + path: &Path, + ) -> Vec<( + LanguageServerId, + Vec>>, + )> { + self.diagnostics.get(path).cloned().unwrap_or_default() + } + + pub fn clear_diagnostics_for_language_server( + &mut self, + server_id: LanguageServerId, + _: &mut ModelContext, + ) { + let worktree_id = self.id().to_proto(); + self.diagnostic_summaries + .retain(|path, summaries_by_server_id| { + if summaries_by_server_id.remove(&server_id).is_some() { + if let Some(share) = self.share.as_ref() { + self.client + .send(proto::UpdateDiagnosticSummary { + project_id: share.project_id, + worktree_id, + summary: Some(proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: 0, + warning_count: 0, + }), + }) + .log_err(); + } + !summaries_by_server_id.is_empty() + } else { + true + } + }); + + self.diagnostics.retain(|_, diagnostics_by_server_id| { + if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + diagnostics_by_server_id.remove(ix); + !diagnostics_by_server_id.is_empty() + } else { + true + } + }); + } + + pub fn update_diagnostics( + &mut self, + server_id: LanguageServerId, + worktree_path: Arc, + diagnostics: Vec>>, + _: &mut ModelContext, + ) -> Result { + let summaries_by_server_id = self + .diagnostic_summaries + .entry(worktree_path.clone()) + .or_default(); + + let old_summary = summaries_by_server_id + .remove(&server_id) + .unwrap_or_default(); + + let new_summary = DiagnosticSummary::new(&diagnostics); + if new_summary.is_empty() { + if let Some(diagnostics_by_server_id) = self.diagnostics.get_mut(&worktree_path) { + if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + diagnostics_by_server_id.remove(ix); + } + if diagnostics_by_server_id.is_empty() { + self.diagnostics.remove(&worktree_path); + } + } + } else { + summaries_by_server_id.insert(server_id, new_summary); + let diagnostics_by_server_id = + self.diagnostics.entry(worktree_path.clone()).or_default(); + match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { + Ok(ix) => { + diagnostics_by_server_id[ix] = (server_id, diagnostics); + } + Err(ix) => { + diagnostics_by_server_id.insert(ix, (server_id, diagnostics)); + } + } + } + + if !old_summary.is_empty() || !new_summary.is_empty() { + if let Some(share) = self.share.as_ref() { + self.client + .send(proto::UpdateDiagnosticSummary { + project_id: share.project_id, + worktree_id: self.id().to_proto(), + summary: Some(proto::DiagnosticSummary { + path: worktree_path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count as u32, + warning_count: new_summary.warning_count as u32, + }), + }) + .log_err(); + } + } + + Ok(!old_summary.is_empty() || !new_summary.is_empty()) + } + + fn set_snapshot( + &mut self, + new_snapshot: LocalSnapshot, + entry_changes: UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot); + + self.snapshot = new_snapshot; + + if let Some(share) = self.share.as_mut() { + share + .snapshots_tx + .unbounded_send(( + self.snapshot.clone(), + entry_changes.clone(), + repo_changes.clone(), + )) + .ok(); + } + + if !entry_changes.is_empty() { + cx.emit(Event::UpdatedEntries(entry_changes)); + } + if !repo_changes.is_empty() { + cx.emit(Event::UpdatedGitRepositories(repo_changes)); + } + } + + fn changed_repos( + &self, + old_snapshot: &LocalSnapshot, + new_snapshot: &LocalSnapshot, + ) -> UpdatedGitRepositoriesSet { + let mut changes = Vec::new(); + let mut old_repos = old_snapshot.git_repositories.iter().peekable(); + let mut new_repos = new_snapshot.git_repositories.iter().peekable(); + loop { + match (new_repos.peek().map(clone), old_repos.peek().map(clone)) { + (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => { + match Ord::cmp(&new_entry_id, &old_entry_id) { + Ordering::Less => { + if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) { + changes.push(( + entry.path.clone(), + GitRepositoryChange { + old_repository: None, + }, + )); + } + new_repos.next(); + } + Ordering::Equal => { + if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id { + if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) { + let old_repo = old_snapshot + .repository_entries + .get(&RepositoryWorkDirectory(entry.path.clone())) + .cloned(); + changes.push(( + entry.path.clone(), + GitRepositoryChange { + old_repository: old_repo, + }, + )); + } + } + new_repos.next(); + old_repos.next(); + } + Ordering::Greater => { + if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) { + let old_repo = old_snapshot + .repository_entries + .get(&RepositoryWorkDirectory(entry.path.clone())) + .cloned(); + changes.push(( + entry.path.clone(), + GitRepositoryChange { + old_repository: old_repo, + }, + )); + } + old_repos.next(); + } + } + } + (Some((entry_id, _)), None) => { + if let Some(entry) = new_snapshot.entry_for_id(entry_id) { + changes.push(( + entry.path.clone(), + GitRepositoryChange { + old_repository: None, + }, + )); + } + new_repos.next(); + } + (None, Some((entry_id, _))) => { + if let Some(entry) = old_snapshot.entry_for_id(entry_id) { + let old_repo = old_snapshot + .repository_entries + .get(&RepositoryWorkDirectory(entry.path.clone())) + .cloned(); + changes.push(( + entry.path.clone(), + GitRepositoryChange { + old_repository: old_repo, + }, + )); + } + old_repos.next(); + } + (None, None) => break, + } + } + + fn clone(value: &(&T, &U)) -> (T, U) { + (value.0.clone(), value.1.clone()) + } + + changes.into() + } + + pub fn scan_complete(&self) -> impl Future { + let mut is_scanning_rx = self.is_scanning.1.clone(); + async move { + let mut is_scanning = is_scanning_rx.borrow().clone(); + while is_scanning { + if let Some(value) = is_scanning_rx.recv().await { + is_scanning = value; + } else { + break; + } + } + } + } + + pub fn snapshot(&self) -> LocalSnapshot { + self.snapshot.clone() + } + + pub fn metadata_proto(&self) -> proto::WorktreeMetadata { + proto::WorktreeMetadata { + id: self.id().to_proto(), + root_name: self.root_name().to_string(), + visible: self.visible, + abs_path: self.abs_path().as_os_str().to_string_lossy().into(), + } + } + + fn load( + &self, + path: &Path, + cx: &mut ModelContext, + ) -> Task)>> { + let path = Arc::from(path); + let abs_path = self.absolutize(&path); + let fs = self.fs.clone(); + let entry = self.refresh_entry(path.clone(), None, cx); + + cx.spawn(|this, cx| async move { + let text = fs.load(&abs_path).await?; + let entry = entry.await?; + + let mut index_task = None; + let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot()); + if let Some(repo) = snapshot.repository_for_path(&path) { + let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap(); + if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) { + let repo = repo.repo_ptr.clone(); + index_task = Some( + cx.background() + .spawn(async move { repo.lock().load_index_text(&repo_path) }), + ); + } + } + + let diff_base = if let Some(index_task) = index_task { + index_task.await + } else { + None + }; + + Ok(( + File { + entry_id: entry.id, + worktree: this, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }, + text, + diff_base, + )) + }) + } + + pub fn save_buffer( + &self, + buffer_handle: Handle, + path: Arc, + has_changed_file: bool, + cx: &mut ModelContext, + ) -> Task> { + let handle = cx.handle(); + let buffer = buffer_handle.read(cx); + + let rpc = self.client.clone(); + let buffer_id = buffer.remote_id(); + let project_id = self.share.as_ref().map(|share| share.project_id); + + let text = buffer.as_rope().clone(); + let fingerprint = text.fingerprint(); + let version = buffer.version(); + let save = self.write_file(path, text, buffer.line_ending(), cx); + + cx.as_mut().spawn(|mut cx| async move { + let entry = save.await?; + + if has_changed_file { + let new_file = Arc::new(File { + entry_id: entry.id, + worktree: handle, + path: entry.path, + mtime: entry.mtime, + is_local: true, + is_deleted: false, + }); + + if let Some(project_id) = project_id { + rpc.send(proto::UpdateBufferFile { + project_id, + buffer_id, + file: Some(new_file.to_proto()), + }) + .log_err(); + } + + buffer_handle.update(&mut cx, |buffer, cx| { + if has_changed_file { + buffer.file_updated(new_file, cx).detach(); + } + }); + } + + if let Some(project_id) = project_id { + rpc.send(proto::BufferSaved { + project_id, + buffer_id, + version: serialize_version(&version), + mtime: Some(entry.mtime.into()), + fingerprint: serialize_fingerprint(fingerprint), + })?; + } + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), fingerprint, entry.mtime, cx); + }); + + Ok(()) + }) + } + + /// Find the lowest path in the worktree's datastructures that is an ancestor + fn lowest_ancestor(&self, path: &Path) -> PathBuf { + let mut lowest_ancestor = None; + for path in path.ancestors() { + if self.entry_for_path(path).is_some() { + lowest_ancestor = Some(path.to_path_buf()); + break; + } + } + + lowest_ancestor.unwrap_or_else(|| PathBuf::from("")) + } + + pub fn create_entry( + &self, + path: impl Into>, + is_dir: bool, + cx: &mut ModelContext, + ) -> Task> { + let path = path.into(); + let lowest_ancestor = self.lowest_ancestor(&path); + let abs_path = self.absolutize(&path); + let fs = self.fs.clone(); + let write = cx.background().spawn(async move { + if is_dir { + fs.create_dir(&abs_path).await + } else { + fs.save(&abs_path, &Default::default(), Default::default()) + .await + } + }); + + cx.spawn(|this, mut cx| async move { + write.await?; + let (result, refreshes) = this.update(&mut cx, |this, cx| { + let mut refreshes = Vec::>>::new(); + let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); + for refresh_path in refresh_paths.ancestors() { + if refresh_path == Path::new("") { + continue; + } + let refresh_full_path = lowest_ancestor.join(refresh_path); + + refreshes.push(this.as_local_mut().unwrap().refresh_entry( + refresh_full_path.into(), + None, + cx, + )); + } + ( + this.as_local_mut().unwrap().refresh_entry(path, None, cx), + refreshes, + ) + }); + for refresh in refreshes { + refresh.await.log_err(); + } + + result.await + }) + } + + pub fn write_file( + &self, + path: impl Into>, + text: Rope, + line_ending: LineEnding, + cx: &mut ModelContext, + ) -> Task> { + let path = path.into(); + let abs_path = self.absolutize(&path); + let fs = self.fs.clone(); + let write = cx + .background() + .spawn(async move { fs.save(&abs_path, &text, line_ending).await }); + + cx.spawn(|this, mut cx| async move { + write.await?; + this.update(&mut cx, |this, cx| { + this.as_local_mut().unwrap().refresh_entry(path, None, cx) + }) + .await + }) + } + + pub fn delete_entry( + &self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let entry = self.entry_for_id(entry_id)?.clone(); + let abs_path = self.absolutize(&entry.path); + let fs = self.fs.clone(); + + let delete = cx.background().spawn(async move { + if entry.is_file() { + fs.remove_file(&abs_path, Default::default()).await?; + } else { + fs.remove_dir( + &abs_path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: false, + }, + ) + .await?; + } + anyhow::Ok(entry.path) + }); + + Some(cx.spawn(|this, mut cx| async move { + let path = delete.await?; + this.update(&mut cx, |this, _| { + this.as_local_mut() + .unwrap() + .refresh_entries_for_paths(vec![path]) + }) + .recv() + .await; + Ok(()) + })) + } + + pub fn rename_entry( + &self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let old_path = self.entry_for_id(entry_id)?.path.clone(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let fs = self.fs.clone(); + let rename = cx.background().spawn(async move { + fs.rename(&abs_old_path, &abs_new_path, Default::default()) + .await + }); + + Some(cx.spawn(|this, mut cx| async move { + rename.await?; + this.update(&mut cx, |this, cx| { + this.as_local_mut() + .unwrap() + .refresh_entry(new_path.clone(), Some(old_path), cx) + }) + .await + })) + } + + pub fn copy_entry( + &self, + entry_id: ProjectEntryId, + new_path: impl Into>, + cx: &mut ModelContext, + ) -> Option>> { + let old_path = self.entry_for_id(entry_id)?.path.clone(); + let new_path = new_path.into(); + let abs_old_path = self.absolutize(&old_path); + let abs_new_path = self.absolutize(&new_path); + let fs = self.fs.clone(); + let copy = cx.background().spawn(async move { + copy_recursive( + fs.as_ref(), + &abs_old_path, + &abs_new_path, + Default::default(), + ) + .await + }); + + Some(cx.spawn(|this, mut cx| async move { + copy.await?; + this.update(&mut cx, |this, cx| { + this.as_local_mut() + .unwrap() + .refresh_entry(new_path.clone(), None, cx) + }) + .await + })) + } + + pub fn expand_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let path = self.entry_for_id(entry_id)?.path.clone(); + let mut refresh = self.refresh_entries_for_paths(vec![path]); + Some(cx.background().spawn(async move { + refresh.next().await; + Ok(()) + })) + } + + pub fn refresh_entries_for_paths(&self, paths: Vec>) -> barrier::Receiver { + let (tx, rx) = barrier::channel(); + self.scan_requests_tx + .try_send(ScanRequest { + relative_paths: paths, + done: tx, + }) + .ok(); + rx + } + + pub fn add_path_prefix_to_scan(&self, path_prefix: Arc) { + self.path_prefixes_to_scan_tx.try_send(path_prefix).ok(); + } + + fn refresh_entry( + &self, + path: Arc, + old_path: Option>, + cx: &mut ModelContext, + ) -> Task> { + let paths = if let Some(old_path) = old_path.as_ref() { + vec![old_path.clone(), path.clone()] + } else { + vec![path.clone()] + }; + let mut refresh = self.refresh_entries_for_paths(paths); + cx.spawn_weak(move |this, mut cx| async move { + refresh.recv().await; + this.upgrade(&cx) + .ok_or_else(|| anyhow!("worktree was dropped"))? + .update(&mut cx, |this, _| { + this.entry_for_path(path) + .cloned() + .ok_or_else(|| anyhow!("failed to read path after update")) + }) + }) + } + + pub fn observe_updates( + &mut self, + project_id: u64, + cx: &mut ModelContext, + callback: F, + ) -> oneshot::Receiver<()> + where + F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, + Fut: Send + Future, + { + #[cfg(any(test, feature = "test-support"))] + const MAX_CHUNK_SIZE: usize = 2; + #[cfg(not(any(test, feature = "test-support")))] + const MAX_CHUNK_SIZE: usize = 256; + + let (share_tx, share_rx) = oneshot::channel(); + + if let Some(share) = self.share.as_mut() { + share_tx.send(()).ok(); + *share.resume_updates.borrow_mut() = (); + return share_rx; + } + + let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>(); + let (snapshots_tx, mut snapshots_rx) = + mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>(); + snapshots_tx + .unbounded_send((self.snapshot(), Arc::from([]), Arc::from([]))) + .ok(); + + let worktree_id = cx.model_id() as u64; + let _maintain_remote_snapshot = cx.background().spawn(async move { + let mut is_first = true; + while let Some((snapshot, entry_changes, repo_changes)) = snapshots_rx.next().await { + let update; + if is_first { + update = snapshot.build_initial_update(project_id, worktree_id); + is_first = false; + } else { + update = + snapshot.build_update(project_id, worktree_id, entry_changes, repo_changes); + } + + for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) { + let _ = resume_updates_rx.try_recv(); + loop { + let result = callback(update.clone()); + if result.await { + break; + } else { + log::info!("waiting to resume updates"); + if resume_updates_rx.next().await.is_none() { + return Some(()); + } + } + } + } + } + share_tx.send(()).ok(); + Some(()) + }); + + self.share = Some(ShareState { + project_id, + snapshots_tx, + resume_updates: resume_updates_tx, + _maintain_remote_snapshot, + }); + share_rx + } + + pub fn share(&mut self, project_id: u64, cx: &mut ModelContext) -> Task> { + let client = self.client.clone(); + + for (path, summaries) in &self.diagnostic_summaries { + for (&server_id, summary) in summaries { + if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id: cx.model_id() as u64, + summary: Some(summary.to_proto(server_id, &path)), + }) { + return Task::ready(Err(e)); + } + } + } + + let rx = self.observe_updates(project_id, cx, move |update| { + client.request(update).map(|result| result.is_ok()) + }); + cx.foreground() + .spawn(async move { rx.await.map_err(|_| anyhow!("share ended")) }) + } + + pub fn unshare(&mut self) { + self.share.take(); + } + + pub fn is_shared(&self) -> bool { + self.share.is_some() + } +} + +impl RemoteWorktree { + fn snapshot(&self) -> Snapshot { + self.snapshot.clone() + } + + pub fn disconnected_from_host(&mut self) { + self.updates_tx.take(); + self.snapshot_subscriptions.clear(); + self.disconnected = true; + } + + pub fn save_buffer( + &self, + buffer_handle: Handle, + cx: &mut ModelContext, + ) -> Task> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); + let version = buffer.version(); + let rpc = self.client.clone(); + let project_id = self.project_id; + cx.as_mut().spawn(|mut cx| async move { + let response = rpc + .request(proto::SaveBuffer { + project_id, + buffer_id, + version: serialize_version(&version), + }) + .await?; + let version = deserialize_version(&response.version); + let fingerprint = deserialize_fingerprint(&response.fingerprint)?; + let mtime = response + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version.clone(), fingerprint, mtime, cx); + }); + + Ok(()) + }) + } + + pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { + if let Some(updates_tx) = &self.updates_tx { + updates_tx + .unbounded_send(update) + .expect("consumer runs to completion"); + } + } + + fn observed_snapshot(&self, scan_id: usize) -> bool { + self.completed_scan_id >= scan_id + } + + pub(crate) fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future> { + let (tx, rx) = oneshot::channel(); + if self.observed_snapshot(scan_id) { + let _ = tx.send(()); + } else if self.disconnected { + drop(tx); + } else { + match self + .snapshot_subscriptions + .binary_search_by_key(&scan_id, |probe| probe.0) + { + Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)), + } + } + + async move { + rx.await?; + Ok(()) + } + } + + pub fn update_diagnostic_summary( + &mut self, + path: Arc, + summary: &proto::DiagnosticSummary, + ) { + let server_id = LanguageServerId(summary.language_server_id as usize); + let summary = DiagnosticSummary { + error_count: summary.error_count as usize, + warning_count: summary.warning_count as usize, + }; + + if summary.is_empty() { + if let Some(summaries) = self.diagnostic_summaries.get_mut(&path) { + summaries.remove(&server_id); + if summaries.is_empty() { + self.diagnostic_summaries.remove(&path); + } + } + } else { + self.diagnostic_summaries + .entry(path) + .or_default() + .insert(server_id, summary); + } + } + + pub fn insert_entry( + &mut self, + entry: proto::Entry, + scan_id: usize, + cx: &mut ModelContext, + ) -> Task> { + let wait_for_snapshot = self.wait_for_snapshot(scan_id); + cx.spawn(|this, mut cx| async move { + wait_for_snapshot.await?; + this.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + let mut snapshot = worktree.background_snapshot.lock(); + let entry = snapshot.insert_entry(entry); + worktree.snapshot = snapshot.clone(); + entry + }) + }) + } + + pub(crate) fn delete_entry( + &mut self, + id: ProjectEntryId, + scan_id: usize, + cx: &mut ModelContext, + ) -> Task> { + let wait_for_snapshot = self.wait_for_snapshot(scan_id); + cx.spawn(|this, mut cx| async move { + wait_for_snapshot.await?; + this.update(&mut cx, |worktree, _| { + let worktree = worktree.as_remote_mut().unwrap(); + let mut snapshot = worktree.background_snapshot.lock(); + snapshot.delete_entry(id); + worktree.snapshot = snapshot.clone(); + }); + Ok(()) + }) + } +} + +impl Snapshot { + pub fn id(&self) -> WorktreeId { + self.id + } + + pub fn abs_path(&self) -> &Arc { + &self.abs_path + } + + pub fn absolutize(&self, path: &Path) -> PathBuf { + if path.file_name().is_some() { + self.abs_path.join(path) + } else { + self.abs_path.to_path_buf() + } + } + + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { + self.entries_by_id.get(&entry_id, &()).is_some() + } + + pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result { + let entry = Entry::try_from((&self.root_char_bag, entry))?; + let old_entry = self.entries_by_id.insert_or_replace( + PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: 0, + }, + &(), + ); + if let Some(old_entry) = old_entry { + self.entries_by_path.remove(&PathKey(old_entry.path), &()); + } + self.entries_by_path.insert_or_replace(entry.clone(), &()); + Ok(entry) + } + + fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option> { + let removed_entry = self.entries_by_id.remove(&entry_id, &())?; + self.entries_by_path = { + let mut cursor = self.entries_by_path.cursor::(); + let mut new_entries_by_path = + cursor.slice(&TraversalTarget::Path(&removed_entry.path), Bias::Left, &()); + while let Some(entry) = cursor.item() { + if entry.path.starts_with(&removed_entry.path) { + self.entries_by_id.remove(&entry.id, &()); + cursor.next(&()); + } else { + break; + } + } + new_entries_by_path.append(cursor.suffix(&()), &()); + new_entries_by_path + }; + + Some(removed_entry.path) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn status_for_file(&self, path: impl Into) -> Option { + let path = path.into(); + self.entries_by_path + .get(&PathKey(Arc::from(path)), &()) + .and_then(|entry| entry.git_status) + } + + pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> { + let mut entries_by_path_edits = Vec::new(); + let mut entries_by_id_edits = Vec::new(); + + for entry_id in update.removed_entries { + let entry_id = ProjectEntryId::from_proto(entry_id); + entries_by_id_edits.push(Edit::Remove(entry_id)); + if let Some(entry) = self.entry_for_id(entry_id) { + entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); + } + } + + for entry in update.updated_entries { + let entry = Entry::try_from((&self.root_char_bag, entry))?; + if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) { + entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); + } + if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), &()) { + if old_entry.id != entry.id { + entries_by_id_edits.push(Edit::Remove(old_entry.id)); + } + } + entries_by_id_edits.push(Edit::Insert(PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: 0, + })); + entries_by_path_edits.push(Edit::Insert(entry)); + } + + self.entries_by_path.edit(entries_by_path_edits, &()); + self.entries_by_id.edit(entries_by_id_edits, &()); + + update.removed_repositories.sort_unstable(); + self.repository_entries.retain(|_, entry| { + if let Ok(_) = update + .removed_repositories + .binary_search(&entry.work_directory.to_proto()) + { + false + } else { + true + } + }); + + for repository in update.updated_repositories { + let work_directory_entry: WorkDirectoryEntry = + ProjectEntryId::from_proto(repository.work_directory_id).into(); + + if let Some(entry) = self.entry_for_id(*work_directory_entry) { + let work_directory = RepositoryWorkDirectory(entry.path.clone()); + if self.repository_entries.get(&work_directory).is_some() { + self.repository_entries.update(&work_directory, |repo| { + repo.branch = repository.branch.map(Into::into); + }); + } else { + self.repository_entries.insert( + work_directory, + RepositoryEntry { + work_directory: work_directory_entry, + branch: repository.branch.map(Into::into), + }, + ) + } + } else { + log::error!("no work directory entry for repository {:?}", repository) + } + } + + self.scan_id = update.scan_id as usize; + if update.is_last_update { + self.completed_scan_id = update.scan_id as usize; + } + + Ok(()) + } + + pub fn file_count(&self) -> usize { + self.entries_by_path.summary().file_count + } + + pub fn visible_file_count(&self) -> usize { + self.entries_by_path.summary().non_ignored_file_count + } + + fn traverse_from_offset( + &self, + include_dirs: bool, + include_ignored: bool, + start_offset: usize, + ) -> Traversal { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek( + &TraversalTarget::Count { + count: start_offset, + include_dirs, + include_ignored, + }, + Bias::Right, + &(), + ); + Traversal { + cursor, + include_dirs, + include_ignored, + } + } + + fn traverse_from_path( + &self, + include_dirs: bool, + include_ignored: bool, + path: &Path, + ) -> Traversal { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek(&TraversalTarget::Path(path), Bias::Left, &()); + Traversal { + cursor, + include_dirs, + include_ignored, + } + } + + pub fn files(&self, include_ignored: bool, start: usize) -> Traversal { + self.traverse_from_offset(false, include_ignored, start) + } + + pub fn entries(&self, include_ignored: bool) -> Traversal { + self.traverse_from_offset(true, include_ignored, 0) + } + + pub fn repositories(&self) -> impl Iterator, &RepositoryEntry)> { + self.repository_entries + .iter() + .map(|(path, entry)| (&path.0, entry)) + } + + /// Get the repository whose work directory contains the given path. + pub fn repository_for_work_directory(&self, path: &Path) -> Option { + self.repository_entries + .get(&RepositoryWorkDirectory(path.into())) + .cloned() + } + + /// Get the repository whose work directory contains the given path. + pub fn repository_for_path(&self, path: &Path) -> Option { + self.repository_and_work_directory_for_path(path) + .map(|e| e.1) + } + + pub fn repository_and_work_directory_for_path( + &self, + path: &Path, + ) -> Option<(RepositoryWorkDirectory, RepositoryEntry)> { + self.repository_entries + .iter() + .filter(|(workdir_path, _)| path.starts_with(workdir_path)) + .last() + .map(|(path, repo)| (path.clone(), repo.clone())) + } + + /// Given an ordered iterator of entries, returns an iterator of those entries, + /// along with their containing git repository. + pub fn entries_with_repositories<'a>( + &'a self, + entries: impl 'a + Iterator, + ) -> impl 'a + Iterator)> { + let mut containing_repos = Vec::<(&Arc, &RepositoryEntry)>::new(); + let mut repositories = self.repositories().peekable(); + entries.map(move |entry| { + while let Some((repo_path, _)) = containing_repos.last() { + if !entry.path.starts_with(repo_path) { + containing_repos.pop(); + } else { + break; + } + } + while let Some((repo_path, _)) = repositories.peek() { + if entry.path.starts_with(repo_path) { + containing_repos.push(repositories.next().unwrap()); + } else { + break; + } + } + let repo = containing_repos.last().map(|(_, repo)| *repo); + (entry, repo) + }) + } + + /// Update the `git_status` of the given entries such that files' + /// statuses bubble up to their ancestor directories. + pub fn propagate_git_statuses(&self, result: &mut [Entry]) { + let mut cursor = self + .entries_by_path + .cursor::<(TraversalProgress, GitStatuses)>(); + let mut entry_stack = Vec::<(usize, GitStatuses)>::new(); + + let mut result_ix = 0; + loop { + let next_entry = result.get(result_ix); + let containing_entry = entry_stack.last().map(|(ix, _)| &result[*ix]); + + let entry_to_finish = match (containing_entry, next_entry) { + (Some(_), None) => entry_stack.pop(), + (Some(containing_entry), Some(next_path)) => { + if !next_path.path.starts_with(&containing_entry.path) { + entry_stack.pop() + } else { + None + } + } + (None, Some(_)) => None, + (None, None) => break, + }; + + if let Some((entry_ix, prev_statuses)) = entry_to_finish { + cursor.seek_forward( + &TraversalTarget::PathSuccessor(&result[entry_ix].path), + Bias::Left, + &(), + ); + + let statuses = cursor.start().1 - prev_statuses; + + result[entry_ix].git_status = if statuses.conflict > 0 { + Some(GitFileStatus::Conflict) + } else if statuses.modified > 0 { + Some(GitFileStatus::Modified) + } else if statuses.added > 0 { + Some(GitFileStatus::Added) + } else { + None + }; + } else { + if result[result_ix].is_dir() { + cursor.seek_forward( + &TraversalTarget::Path(&result[result_ix].path), + Bias::Left, + &(), + ); + entry_stack.push((result_ix, cursor.start().1)); + } + result_ix += 1; + } + } + } + + pub fn paths(&self) -> impl Iterator> { + let empty_path = Path::new(""); + self.entries_by_path + .cursor::<()>() + .filter(move |entry| entry.path.as_ref() != empty_path) + .map(|entry| &entry.path) + } + + fn child_entries<'a>(&'a self, parent_path: &'a Path) -> ChildEntriesIter<'a> { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &()); + let traversal = Traversal { + cursor, + include_dirs: true, + include_ignored: true, + }; + ChildEntriesIter { + traversal, + parent_path, + } + } + + pub fn descendent_entries<'a>( + &'a self, + include_dirs: bool, + include_ignored: bool, + parent_path: &'a Path, + ) -> DescendentEntriesIter<'a> { + let mut cursor = self.entries_by_path.cursor(); + cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &()); + let mut traversal = Traversal { + cursor, + include_dirs, + include_ignored, + }; + + if traversal.end_offset() == traversal.start_offset() { + traversal.advance(); + } + + DescendentEntriesIter { + traversal, + parent_path, + } + } + + pub fn root_entry(&self) -> Option<&Entry> { + self.entry_for_path("") + } + + pub fn root_name(&self) -> &str { + &self.root_name + } + + pub fn root_git_entry(&self) -> Option { + self.repository_entries + .get(&RepositoryWorkDirectory(Path::new("").into())) + .map(|entry| entry.to_owned()) + } + + pub fn git_entries(&self) -> impl Iterator { + self.repository_entries.values() + } + + pub fn scan_id(&self) -> usize { + self.scan_id + } + + pub fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { + let path = path.as_ref(); + self.traverse_from_path(true, true, path) + .entry() + .and_then(|entry| { + if entry.path.as_ref() == path { + Some(entry) + } else { + None + } + }) + } + + pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { + let entry = self.entries_by_id.get(&id, &())?; + self.entry_for_path(&entry.path) + } + + pub fn inode_for_path(&self, path: impl AsRef) -> Option { + self.entry_for_path(path.as_ref()).map(|e| e.inode) + } +} + +impl LocalSnapshot { + pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> { + self.git_repositories.get(&repo.work_directory.0) + } + + pub(crate) fn local_repo_for_path( + &self, + path: &Path, + ) -> Option<(RepositoryWorkDirectory, &LocalRepositoryEntry)> { + let (path, repo) = self.repository_and_work_directory_for_path(path)?; + Some((path, self.git_repositories.get(&repo.work_directory_id())?)) + } + + fn build_update( + &self, + project_id: u64, + worktree_id: u64, + entry_changes: UpdatedEntriesSet, + repo_changes: UpdatedGitRepositoriesSet, + ) -> proto::UpdateWorktree { + let mut updated_entries = Vec::new(); + let mut removed_entries = Vec::new(); + let mut updated_repositories = Vec::new(); + let mut removed_repositories = Vec::new(); + + for (_, entry_id, path_change) in entry_changes.iter() { + if let PathChange::Removed = path_change { + removed_entries.push(entry_id.0 as u64); + } else if let Some(entry) = self.entry_for_id(*entry_id) { + updated_entries.push(proto::Entry::from(entry)); + } + } + + for (work_dir_path, change) in repo_changes.iter() { + let new_repo = self + .repository_entries + .get(&RepositoryWorkDirectory(work_dir_path.clone())); + match (&change.old_repository, new_repo) { + (Some(old_repo), Some(new_repo)) => { + updated_repositories.push(new_repo.build_update(old_repo)); + } + (None, Some(new_repo)) => { + updated_repositories.push(proto::RepositoryEntry::from(new_repo)); + } + (Some(old_repo), None) => { + removed_repositories.push(old_repo.work_directory.0.to_proto()); + } + _ => {} + } + } + + removed_entries.sort_unstable(); + updated_entries.sort_unstable_by_key(|e| e.id); + removed_repositories.sort_unstable(); + updated_repositories.sort_unstable_by_key(|e| e.work_directory_id); + + // TODO - optimize, knowing that removed_entries are sorted. + removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err()); + + proto::UpdateWorktree { + project_id, + worktree_id, + abs_path: self.abs_path().to_string_lossy().into(), + root_name: self.root_name().to_string(), + updated_entries, + removed_entries, + scan_id: self.scan_id as u64, + is_last_update: self.completed_scan_id == self.scan_id, + updated_repositories, + removed_repositories, + } + } + + fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree { + let mut updated_entries = self + .entries_by_path + .iter() + .map(proto::Entry::from) + .collect::>(); + updated_entries.sort_unstable_by_key(|e| e.id); + + let mut updated_repositories = self + .repository_entries + .values() + .map(proto::RepositoryEntry::from) + .collect::>(); + updated_repositories.sort_unstable_by_key(|e| e.work_directory_id); + + proto::UpdateWorktree { + project_id, + worktree_id, + abs_path: self.abs_path().to_string_lossy().into(), + root_name: self.root_name().to_string(), + updated_entries, + removed_entries: Vec::new(), + scan_id: self.scan_id as u64, + is_last_update: self.completed_scan_id == self.scan_id, + updated_repositories, + removed_repositories: Vec::new(), + } + } + + fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { + let abs_path = self.abs_path.join(&entry.path); + match smol::block_on(build_gitignore(&abs_path, fs)) { + Ok(ignore) => { + self.ignores_by_parent_abs_path + .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); + } + Err(error) => { + log::error!( + "error loading .gitignore file {:?} - {:?}", + &entry.path, + error + ); + } + } + } + + if entry.kind == EntryKind::PendingDir { + if let Some(existing_entry) = + self.entries_by_path.get(&PathKey(entry.path.clone()), &()) + { + entry.kind = existing_entry.kind; + } + } + + let scan_id = self.scan_id; + let removed = self.entries_by_path.insert_or_replace(entry.clone(), &()); + if let Some(removed) = removed { + if removed.id != entry.id { + self.entries_by_id.remove(&removed.id, &()); + } + } + self.entries_by_id.insert_or_replace( + PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id, + }, + &(), + ); + + entry + } + + fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet { + let mut inodes = TreeSet::default(); + for ancestor in path.ancestors().skip(1) { + if let Some(entry) = self.entry_for_path(ancestor) { + inodes.insert(entry.inode); + } + } + inodes + } + + fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc { + let mut new_ignores = Vec::new(); + for ancestor in abs_path.ancestors().skip(1) { + if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { + new_ignores.push((ancestor, Some(ignore.clone()))); + } else { + new_ignores.push((ancestor, None)); + } + } + + let mut ignore_stack = IgnoreStack::none(); + for (parent_abs_path, ignore) in new_ignores.into_iter().rev() { + if ignore_stack.is_abs_path_ignored(parent_abs_path, true) { + ignore_stack = IgnoreStack::all(); + break; + } else if let Some(ignore) = ignore { + ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore); + } + } + + if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { + ignore_stack = IgnoreStack::all(); + } + + ignore_stack + } + + #[cfg(test)] + pub(crate) fn expanded_entries(&self) -> impl Iterator { + self.entries_by_path + .cursor::<()>() + .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored)) + } + + #[cfg(test)] + pub fn check_invariants(&self, git_state: bool) { + use pretty_assertions::assert_eq; + + assert_eq!( + self.entries_by_path + .cursor::<()>() + .map(|e| (&e.path, e.id)) + .collect::>(), + self.entries_by_id + .cursor::<()>() + .map(|e| (&e.path, e.id)) + .collect::>() + .into_iter() + .collect::>(), + "entries_by_path and entries_by_id are inconsistent" + ); + + let mut files = self.files(true, 0); + let mut visible_files = self.files(false, 0); + for entry in self.entries_by_path.cursor::<()>() { + if entry.is_file() { + assert_eq!(files.next().unwrap().inode, entry.inode); + if !entry.is_ignored && !entry.is_external { + assert_eq!(visible_files.next().unwrap().inode, entry.inode); + } + } + } + + assert!(files.next().is_none()); + assert!(visible_files.next().is_none()); + + let mut bfs_paths = Vec::new(); + let mut stack = self + .root_entry() + .map(|e| e.path.as_ref()) + .into_iter() + .collect::>(); + while let Some(path) = stack.pop() { + bfs_paths.push(path); + let ix = stack.len(); + for child_entry in self.child_entries(path) { + stack.insert(ix, &child_entry.path); + } + } + + let dfs_paths_via_iter = self + .entries_by_path + .cursor::<()>() + .map(|e| e.path.as_ref()) + .collect::>(); + assert_eq!(bfs_paths, dfs_paths_via_iter); + + let dfs_paths_via_traversal = self + .entries(true) + .map(|e| e.path.as_ref()) + .collect::>(); + assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); + + if git_state { + for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { + let ignore_parent_path = + ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); + assert!(self.entry_for_path(&ignore_parent_path).is_some()); + assert!(self + .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) + .is_some()); + } + } + } + + #[cfg(test)] + pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> { + let mut paths = Vec::new(); + for entry in self.entries_by_path.cursor::<()>() { + if include_ignored || !entry.is_ignored { + paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored)); + } + } + paths.sort_by(|a, b| a.0.cmp(b.0)); + paths + } +} + +impl BackgroundScannerState { + fn should_scan_directory(&self, entry: &Entry) -> bool { + (!entry.is_external && !entry.is_ignored) + || entry.path.file_name() == Some(&*DOT_GIT) + || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning + || self + .paths_to_scan + .iter() + .any(|p| p.starts_with(&entry.path)) + || self + .path_prefixes_to_scan + .iter() + .any(|p| entry.path.starts_with(p)) + } + + fn enqueue_scan_dir(&self, abs_path: Arc, entry: &Entry, scan_job_tx: &Sender) { + let path = entry.path.clone(); + let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); + let mut containing_repository = None; + if !ignore_stack.is_all() { + if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { + if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { + containing_repository = Some(( + workdir_path, + repo.repo_ptr.clone(), + repo.repo_ptr.lock().staged_statuses(repo_path), + )); + } + } + } + if !ancestor_inodes.contains(&entry.inode) { + ancestor_inodes.insert(entry.inode); + scan_job_tx + .try_send(ScanJob { + abs_path, + path, + ignore_stack, + scan_queue: scan_job_tx.clone(), + ancestor_inodes, + is_external: entry.is_external, + containing_repository, + }) + .unwrap(); + } + } + + fn reuse_entry_id(&mut self, entry: &mut Entry) { + if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { + entry.id = removed_entry_id; + } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) { + entry.id = existing_entry.id; + } + } + + fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + self.reuse_entry_id(&mut entry); + let entry = self.snapshot.insert_entry(entry, fs); + if entry.path.file_name() == Some(&DOT_GIT) { + self.build_git_repository(entry.path.clone(), fs); + } + + #[cfg(test)] + self.snapshot.check_invariants(false); + + entry + } + + fn populate_dir( + &mut self, + parent_path: &Arc, + entries: impl IntoIterator, + ignore: Option>, + ) { + let mut parent_entry = if let Some(parent_entry) = self + .snapshot + .entries_by_path + .get(&PathKey(parent_path.clone()), &()) + { + parent_entry.clone() + } else { + log::warn!( + "populating a directory {:?} that has been removed", + parent_path + ); + return; + }; + + match parent_entry.kind { + EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir, + EntryKind::Dir => {} + _ => return, + } + + if let Some(ignore) = ignore { + let abs_parent_path = self.snapshot.abs_path.join(&parent_path).into(); + self.snapshot + .ignores_by_parent_abs_path + .insert(abs_parent_path, (ignore, false)); + } + + let parent_entry_id = parent_entry.id; + self.scanned_dirs.insert(parent_entry_id); + let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; + let mut entries_by_id_edits = Vec::new(); + + for entry in entries { + entries_by_id_edits.push(Edit::Insert(PathEntry { + id: entry.id, + path: entry.path.clone(), + is_ignored: entry.is_ignored, + scan_id: self.snapshot.scan_id, + })); + entries_by_path_edits.push(Edit::Insert(entry)); + } + + self.snapshot + .entries_by_path + .edit(entries_by_path_edits, &()); + self.snapshot.entries_by_id.edit(entries_by_id_edits, &()); + + if let Err(ix) = self.changed_paths.binary_search(parent_path) { + self.changed_paths.insert(ix, parent_path.clone()); + } + + #[cfg(test)] + self.snapshot.check_invariants(false); + } + + fn remove_path(&mut self, path: &Path) { + let mut new_entries; + let removed_entries; + { + let mut cursor = self.snapshot.entries_by_path.cursor::(); + new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &()); + removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &()); + new_entries.append(cursor.suffix(&()), &()); + } + self.snapshot.entries_by_path = new_entries; + + let mut entries_by_id_edits = Vec::new(); + for entry in removed_entries.cursor::<()>() { + let removed_entry_id = self + .removed_entry_ids + .entry(entry.inode) + .or_insert(entry.id); + *removed_entry_id = cmp::max(*removed_entry_id, entry.id); + entries_by_id_edits.push(Edit::Remove(entry.id)); + } + self.snapshot.entries_by_id.edit(entries_by_id_edits, &()); + + if path.file_name() == Some(&GITIGNORE) { + let abs_parent_path = self.snapshot.abs_path.join(path.parent().unwrap()); + if let Some((_, needs_update)) = self + .snapshot + .ignores_by_parent_abs_path + .get_mut(abs_parent_path.as_path()) + { + *needs_update = true; + } + } + + #[cfg(test)] + self.snapshot.check_invariants(false); + } + + fn reload_repositories(&mut self, changed_paths: &[Arc], 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); + + // If there is already a repository for this .git directory, reload + // the status for all of its files. + let repository = self + .snapshot + .git_repositories + .iter() + .find_map(|(entry_id, repo)| { + (repo.git_dir_path.as_ref() == dot_git_dir).then(|| (*entry_id, repo.clone())) + }); + match repository { + None => { + self.build_git_repository(dot_git_dir.into(), fs); + } + Some((entry_id, repository)) => { + if repository.git_dir_scan_id == scan_id { + continue; + } + let Some(work_dir) = self + .snapshot + .entry_for_id(entry_id) + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + else { + continue; + }; + + log::info!("reload git repository {:?}", dot_git_dir); + let repository = repository.repo_ptr.lock(); + let branch = repository.branch_name(); + repository.reload_index(); + + self.snapshot + .git_repositories + .update(&entry_id, |entry| entry.git_dir_scan_id = scan_id); + self.snapshot + .snapshot + .repository_entries + .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); + + self.update_git_statuses(&work_dir, &*repository); + } + } + } + + // Remove any git repositories whose .git entry no longer exists. + let snapshot = &mut self.snapshot; + let mut repositories = mem::take(&mut snapshot.git_repositories); + let mut repository_entries = mem::take(&mut snapshot.repository_entries); + repositories.retain(|work_directory_id, _| { + snapshot + .entry_for_id(*work_directory_id) + .map_or(false, |entry| { + snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() + }) + }); + repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some()); + snapshot.git_repositories = repositories; + snapshot.repository_entries = repository_entries; + } + + fn build_git_repository( + &mut self, + dot_git_path: Arc, + fs: &dyn Fs, + ) -> Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )> { + log::info!("build git repository {:?}", dot_git_path); + + let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); + + // Guard against repositories inside the repository metadata + if work_dir_path.iter().any(|component| component == *DOT_GIT) { + return None; + }; + + let work_dir_id = self + .snapshot + .entry_for_path(work_dir_path.clone()) + .map(|entry| entry.id)?; + + if self.snapshot.git_repositories.get(&work_dir_id).is_some() { + return None; + } + + let abs_path = self.snapshot.abs_path.join(&dot_git_path); + let repository = fs.open_repo(abs_path.as_path())?; + let work_directory = RepositoryWorkDirectory(work_dir_path.clone()); + + let repo_lock = repository.lock(); + self.snapshot.repository_entries.insert( + work_directory.clone(), + RepositoryEntry { + work_directory: work_dir_id.into(), + branch: repo_lock.branch_name().map(Into::into), + }, + ); + + let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock); + drop(repo_lock); + + self.snapshot.git_repositories.insert( + work_dir_id, + LocalRepositoryEntry { + git_dir_scan_id: 0, + repo_ptr: repository.clone(), + git_dir_path: dot_git_path.clone(), + }, + ); + + Some((work_directory, repository, staged_statuses)) + } + + fn update_git_statuses( + &mut self, + work_directory: &RepositoryWorkDirectory, + repo: &dyn GitRepository, + ) -> TreeMap { + let staged_statuses = repo.staged_statuses(Path::new("")); + + let mut changes = vec![]; + let mut edits = vec![]; + + for mut entry in self + .snapshot + .descendent_entries(false, false, &work_directory.0) + .cloned() + { + let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else { + continue; + }; + let repo_path = RepoPath(repo_path.to_path_buf()); + let git_file_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repo.unstaged_status(&repo_path, entry.mtime), + ); + if entry.git_status != git_file_status { + entry.git_status = git_file_status; + changes.push(entry.path.clone()); + edits.push(Edit::Insert(entry)); + } + } + + self.snapshot.entries_by_path.edit(edits, &()); + util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp); + staged_statuses + } +} + +async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { + let contents = fs.load(abs_path).await?; + let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); + let mut builder = GitignoreBuilder::new(parent); + for line in contents.lines() { + builder.add_line(Some(abs_path.into()), line)?; + } + Ok(builder.build()?) +} + +impl WorktreeId { + pub fn from_usize(handle_id: usize) -> Self { + Self(handle_id) + } + + pub(crate) fn from_proto(id: u64) -> Self { + Self(id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + + pub fn to_usize(&self) -> usize { + self.0 + } +} + +impl fmt::Display for WorktreeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Deref for Worktree { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + match self { + Worktree::Local(worktree) => &worktree.snapshot, + Worktree::Remote(worktree) => &worktree.snapshot, + } + } +} + +impl Deref for LocalWorktree { + type Target = LocalSnapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl Deref for RemoteWorktree { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl fmt::Debug for LocalWorktree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.snapshot.fmt(f) + } +} + +impl fmt::Debug for Snapshot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct EntriesById<'a>(&'a SumTree); + struct EntriesByPath<'a>(&'a SumTree); + + impl<'a> fmt::Debug for EntriesByPath<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map() + .entries(self.0.iter().map(|entry| (&entry.path, entry.id))) + .finish() + } + } + + impl<'a> fmt::Debug for EntriesById<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } + } + + f.debug_struct("Snapshot") + .field("id", &self.id) + .field("root_name", &self.root_name) + .field("entries_by_path", &EntriesByPath(&self.entries_by_path)) + .field("entries_by_id", &EntriesById(&self.entries_by_id)) + .finish() + } +} + +#[derive(Clone, PartialEq)] +pub struct File { + pub worktree: Handle, + pub path: Arc, + pub mtime: SystemTime, + pub(crate) entry_id: ProjectEntryId, + pub(crate) is_local: bool, + pub(crate) is_deleted: bool, +} + +impl language2::File for File { + fn as_local(&self) -> Option<&dyn language2::LocalFile> { + if self.is_local { + Some(self) + } else { + None + } + } + + fn mtime(&self) -> SystemTime { + self.mtime + } + + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, cx: &AppContext) -> PathBuf { + let mut full_path = PathBuf::new(); + let worktree = self.worktree.read(cx); + + if worktree.is_visible() { + full_path.push(worktree.root_name()); + } else { + let path = worktree.abs_path(); + + if worktree.is_local() && path.starts_with(HOME.as_path()) { + full_path.push("~"); + full_path.push(path.strip_prefix(HOME.as_path()).unwrap()); + } else { + full_path.push(path) + } + } + + if self.path.components().next().is_some() { + full_path.push(&self.path); + } + + full_path + } + + /// Returns the last component of this handle's absolute path. If this handle refers to the root + /// of its worktree, then this method will return the name of the worktree itself. + fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr { + self.path + .file_name() + .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) + } + + fn worktree_id(&self) -> usize { + self.worktree.id() + } + + fn is_deleted(&self) -> bool { + self.is_deleted + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn to_proto(&self) -> rpc::proto::File { + rpc::proto::File { + worktree_id: self.worktree.id() as u64, + entry_id: self.entry_id.to_proto(), + path: self.path.to_string_lossy().into(), + mtime: Some(self.mtime.into()), + is_deleted: self.is_deleted, + } + } +} + +impl language2::LocalFile for File { + fn abs_path(&self, cx: &AppContext) -> PathBuf { + self.worktree + .read(cx) + .as_local() + .unwrap() + .abs_path + .join(&self.path) + } + + fn load(&self, cx: &AppContext) -> Task> { + let worktree = self.worktree.read(cx).as_local().unwrap(); + let abs_path = worktree.absolutize(&self.path); + let fs = worktree.fs.clone(); + cx.background() + .spawn(async move { fs.load(&abs_path).await }) + } + + fn buffer_reloaded( + &self, + buffer_id: u64, + version: &clock::Global, + fingerprint: RopeFingerprint, + line_ending: LineEnding, + mtime: SystemTime, + cx: &mut AppContext, + ) { + let worktree = self.worktree.read(cx).as_local().unwrap(); + if let Some(project_id) = worktree.share.as_ref().map(|share| share.project_id) { + worktree + .client + .send(proto::BufferReloaded { + project_id, + buffer_id, + version: serialize_version(version), + mtime: Some(mtime.into()), + fingerprint: serialize_fingerprint(fingerprint), + line_ending: serialize_line_ending(line_ending) as i32, + }) + .log_err(); + } + } +} + +impl File { + pub fn for_entry(entry: Entry, worktree: Handle) -> Arc { + Arc::new(Self { + worktree, + path: entry.path.clone(), + mtime: entry.mtime, + entry_id: entry.id, + is_local: true, + is_deleted: false, + }) + } + + pub fn from_proto( + proto: rpc::proto::File, + worktree: Handle, + cx: &AppContext, + ) -> Result { + let worktree_id = worktree + .read(cx) + .as_remote() + .ok_or_else(|| anyhow!("not remote"))? + .id(); + + if worktree_id.to_proto() != proto.worktree_id { + return Err(anyhow!("worktree id does not match file")); + } + + Ok(Self { + worktree, + path: Path::new(&proto.path).into(), + mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), + entry_id: ProjectEntryId::from_proto(proto.entry_id), + is_local: false, + is_deleted: proto.is_deleted, + }) + } + + pub fn from_dyn(file: Option<&Arc>) -> Option<&Self> { + file.and_then(|f| f.as_any().downcast_ref()) + } + + pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId { + self.worktree.read(cx).id() + } + + pub fn project_entry_id(&self, _: &AppContext) -> Option { + if self.is_deleted { + None + } else { + Some(self.entry_id) + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Entry { + pub id: ProjectEntryId, + pub kind: EntryKind, + pub path: Arc, + pub inode: u64, + pub mtime: SystemTime, + pub is_symlink: bool, + + /// Whether this entry is ignored by Git. + /// + /// We only scan ignored entries once the directory is expanded and + /// exclude them from searches. + pub is_ignored: bool, + + /// Whether this entry's canonical path is outside of the worktree. + /// This means the entry is only accessible from the worktree root via a + /// symlink. + /// + /// We only scan entries outside of the worktree once the symlinked + /// directory is expanded. External entries are treated like gitignored + /// entries in that they are not included in searches. + pub is_external: bool, + pub git_status: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EntryKind { + UnloadedDir, + PendingDir, + Dir, + File(CharBag), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PathChange { + /// A filesystem entry was was created. + Added, + /// A filesystem entry was removed. + Removed, + /// A filesystem entry was updated. + Updated, + /// A filesystem entry was either updated or added. We don't know + /// whether or not it already existed, because the path had not + /// been loaded before the event. + AddedOrUpdated, + /// A filesystem entry was found during the initial scan of the worktree. + Loaded, +} + +pub struct GitRepositoryChange { + /// The previous state of the repository, if it already existed. + pub old_repository: Option, +} + +pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; +pub type UpdatedGitRepositoriesSet = Arc<[(Arc, GitRepositoryChange)]>; + +impl Entry { + fn new( + path: Arc, + metadata: &fs::Metadata, + next_entry_id: &AtomicUsize, + root_char_bag: CharBag, + ) -> Self { + Self { + id: ProjectEntryId::new(next_entry_id), + kind: if metadata.is_dir { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &path)) + }, + path, + inode: metadata.inode, + mtime: metadata.mtime, + is_symlink: metadata.is_symlink, + is_ignored: false, + is_external: false, + git_status: None, + } + } + + pub fn is_dir(&self) -> bool { + self.kind.is_dir() + } + + pub fn is_file(&self) -> bool { + self.kind.is_file() + } + + pub fn git_status(&self) -> Option { + self.git_status + } +} + +impl EntryKind { + pub fn is_dir(&self) -> bool { + matches!( + self, + EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir + ) + } + + pub fn is_unloaded(&self) -> bool { + matches!(self, EntryKind::UnloadedDir) + } + + pub fn is_file(&self) -> bool { + matches!(self, EntryKind::File(_)) + } +} + +impl sum_tree::Item for Entry { + type Summary = EntrySummary; + + fn summary(&self) -> Self::Summary { + let non_ignored_count = if self.is_ignored || self.is_external { + 0 + } else { + 1 + }; + let file_count; + let non_ignored_file_count; + if self.is_file() { + file_count = 1; + non_ignored_file_count = non_ignored_count; + } else { + file_count = 0; + non_ignored_file_count = 0; + } + + let mut statuses = GitStatuses::default(); + match self.git_status { + Some(status) => match status { + GitFileStatus::Added => statuses.added = 1, + GitFileStatus::Modified => statuses.modified = 1, + GitFileStatus::Conflict => statuses.conflict = 1, + }, + None => {} + } + + EntrySummary { + max_path: self.path.clone(), + count: 1, + non_ignored_count, + file_count, + non_ignored_file_count, + statuses, + } + } +} + +impl sum_tree::KeyedItem for Entry { + type Key = PathKey; + + fn key(&self) -> Self::Key { + PathKey(self.path.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct EntrySummary { + max_path: Arc, + count: usize, + non_ignored_count: usize, + file_count: usize, + non_ignored_file_count: usize, + statuses: GitStatuses, +} + +impl Default for EntrySummary { + fn default() -> Self { + Self { + max_path: Arc::from(Path::new("")), + count: 0, + non_ignored_count: 0, + file_count: 0, + non_ignored_file_count: 0, + statuses: Default::default(), + } + } +} + +impl sum_tree::Summary for EntrySummary { + type Context = (); + + fn add_summary(&mut self, rhs: &Self, _: &()) { + self.max_path = rhs.max_path.clone(); + self.count += rhs.count; + self.non_ignored_count += rhs.non_ignored_count; + self.file_count += rhs.file_count; + self.non_ignored_file_count += rhs.non_ignored_file_count; + self.statuses += rhs.statuses; + } +} + +#[derive(Clone, Debug)] +struct PathEntry { + id: ProjectEntryId, + path: Arc, + is_ignored: bool, + scan_id: usize, +} + +impl sum_tree::Item for PathEntry { + type Summary = PathEntrySummary; + + fn summary(&self) -> Self::Summary { + PathEntrySummary { max_id: self.id } + } +} + +impl sum_tree::KeyedItem for PathEntry { + type Key = ProjectEntryId; + + fn key(&self) -> Self::Key { + self.id + } +} + +#[derive(Clone, Debug, Default)] +struct PathEntrySummary { + max_id: ProjectEntryId, +} + +impl sum_tree::Summary for PathEntrySummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + self.max_id = summary.max_id; + } +} + +impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { + fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) { + *self = summary.max_id; + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct PathKey(Arc); + +impl Default for PathKey { + fn default() -> Self { + Self(Path::new("").into()) + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + self.0 = summary.max_path.clone(); + } +} + +struct BackgroundScanner { + state: Mutex, + fs: Arc, + status_updates_tx: UnboundedSender, + executor: Executor, + scan_requests_rx: channel::Receiver, + path_prefixes_to_scan_rx: channel::Receiver>, + next_entry_id: Arc, + phase: BackgroundScannerPhase, +} + +#[derive(PartialEq)] +enum BackgroundScannerPhase { + InitialScan, + EventsReceivedDuringInitialScan, + Events, +} + +impl BackgroundScanner { + fn new( + snapshot: LocalSnapshot, + next_entry_id: Arc, + fs: Arc, + status_updates_tx: UnboundedSender, + executor: Executor, + scan_requests_rx: channel::Receiver, + path_prefixes_to_scan_rx: channel::Receiver>, + ) -> Self { + Self { + fs, + status_updates_tx, + executor, + scan_requests_rx, + path_prefixes_to_scan_rx, + next_entry_id, + state: Mutex::new(BackgroundScannerState { + prev_snapshot: snapshot.snapshot.clone(), + snapshot, + scanned_dirs: Default::default(), + path_prefixes_to_scan: Default::default(), + paths_to_scan: Default::default(), + removed_entry_ids: Default::default(), + changed_paths: Default::default(), + }), + phase: BackgroundScannerPhase::InitialScan, + } + } + + async fn run( + &mut self, + mut fs_events_rx: Pin>>>, + ) { + use futures::FutureExt as _; + + // Populate ignores above the root. + let root_abs_path = self.state.lock().snapshot.abs_path.clone(); + for ancestor in root_abs_path.ancestors().skip(1) { + if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await + { + self.state + .lock() + .snapshot + .ignores_by_parent_abs_path + .insert(ancestor.into(), (ignore.into(), false)); + } + } + + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + { + let mut state = self.state.lock(); + state.snapshot.scan_id += 1; + if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(&root_abs_path, true); + if ignore_stack.is_all() { + root_entry.is_ignored = true; + state.insert_entry(root_entry.clone(), self.fs.as_ref()); + } + state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx); + } + }; + + // Perform an initial scan of the directory. + drop(scan_job_tx); + self.scan_dirs(true, scan_job_rx).await; + { + let mut state = self.state.lock(); + state.snapshot.completed_scan_id = state.snapshot.scan_id; + } + + self.send_status_update(false, None); + + // Process any any FS events that occurred while performing the initial scan. + // For these events, update events cannot be as precise, because we didn't + // have the previous state loaded yet. + self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan; + if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) { + let mut paths = events.into_iter().map(|e| e.path).collect::>(); + while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { + paths.extend(more_events.into_iter().map(|e| e.path)); + } + self.process_events(paths).await; + } + + // Continue processing events until the worktree is dropped. + self.phase = BackgroundScannerPhase::Events; + loop { + select_biased! { + // Process any path refresh requests from the worktree. Prioritize + // these before handling changes reported by the filesystem. + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, false).await { + return; + } + } + + path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => { + let Ok(path_prefix) = path_prefix else { break }; + log::trace!("adding path prefix {:?}", path_prefix); + + let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await; + if did_scan { + let abs_path = + { + let mut state = self.state.lock(); + state.path_prefixes_to_scan.insert(path_prefix.clone()); + state.snapshot.abs_path.join(&path_prefix) + }; + + if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() { + self.process_events(vec![abs_path]).await; + } + } + } + + events = fs_events_rx.next().fuse() => { + let Some(events) = events else { break }; + let mut paths = events.into_iter().map(|e| e.path).collect::>(); + while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { + paths.extend(more_events.into_iter().map(|e| e.path)); + } + self.process_events(paths.clone()).await; + } + } + } + } + + async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool { + log::debug!("rescanning paths {:?}", request.relative_paths); + + request.relative_paths.sort_unstable(); + self.forcibly_load_paths(&request.relative_paths).await; + + let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_canonical_path = match self.fs.canonicalize(&root_path).await { + Ok(path) => path, + Err(err) => { + log::error!("failed to canonicalize root path: {}", err); + return false; + } + }; + let abs_paths = request + .relative_paths + .iter() + .map(|path| { + if path.file_name().is_some() { + root_canonical_path.join(path) + } else { + root_canonical_path.clone() + } + }) + .collect::>(); + + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &request.relative_paths, + abs_paths, + None, + ) + .await; + self.send_status_update(scanning, Some(request.done)) + } + + async fn process_events(&mut self, mut abs_paths: Vec) { + let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_canonical_path = match self.fs.canonicalize(&root_path).await { + Ok(path) => path, + Err(err) => { + log::error!("failed to canonicalize root path: {}", err); + return; + } + }; + + let mut relative_paths = Vec::with_capacity(abs_paths.len()); + 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 relative_path: Arc = + if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { + path.into() + } else { + log::error!( + "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}", + ); + 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; + } + + relative_paths.push(relative_path); + true + } + }); + + if relative_paths.is_empty() { + return; + } + + 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.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()); + 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); + } + } + + self.send_status_update(false, None); + } + + async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { + let (scan_job_tx, mut scan_job_rx) = channel::unbounded(); + { + let mut state = self.state.lock(); + let root_path = state.snapshot.abs_path.clone(); + for path in paths { + for ancestor in path.ancestors() { + if let Some(entry) = state.snapshot.entry_for_path(ancestor) { + if entry.kind == EntryKind::UnloadedDir { + let abs_path = root_path.join(ancestor); + state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); + state.paths_to_scan.insert(path.clone()); + break; + } + } + } + } + drop(scan_job_tx); + } + while let Some(job) = scan_job_rx.next().await { + self.scan_dir(&job).await.log_err(); + } + + mem::take(&mut self.state.lock().paths_to_scan).len() > 0 + } + + async fn scan_dirs( + &self, + enable_progress_updates: bool, + scan_jobs_rx: channel::Receiver, + ) { + use futures::FutureExt as _; + + if self + .status_updates_tx + .unbounded_send(ScanState::Started) + .is_err() + { + return; + } + + let progress_update_count = AtomicUsize::new(0); + self.executor + .scoped(|scope| { + for _ in 0..self.executor.num_cpus() { + scope.spawn(async { + let mut last_progress_update_count = 0; + let progress_update_timer = self.progress_timer(enable_progress_updates).fuse(); + futures::pin_mut!(progress_update_timer); + + loop { + select_biased! { + // Process any path refresh requests before moving on to process + // the scan queue, so that user operations are prioritized. + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, true).await { + return; + } + } + + // Send periodic progress updates to the worktree. Use an atomic counter + // to ensure that only one of the workers sends a progress update after + // the update interval elapses. + _ = progress_update_timer => { + match progress_update_count.compare_exchange( + last_progress_update_count, + last_progress_update_count + 1, + SeqCst, + SeqCst + ) { + Ok(_) => { + last_progress_update_count += 1; + self.send_status_update(true, None); + } + Err(count) => { + last_progress_update_count = count; + } + } + progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse()); + } + + // Recursively load directories from the file system. + job = scan_jobs_rx.recv().fuse() => { + let Ok(job) = job else { break }; + if let Err(err) = self.scan_dir(&job).await { + if job.path.as_ref() != Path::new("") { + log::error!("error scanning directory {:?}: {}", job.abs_path, err); + } + } + } + } + } + }) + } + }) + .await; + } + + fn send_status_update(&self, scanning: bool, barrier: Option) -> bool { + let mut state = self.state.lock(); + if state.changed_paths.is_empty() && scanning { + return true; + } + + let new_snapshot = state.snapshot.clone(); + let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone()); + let changes = self.build_change_set(&old_snapshot, &new_snapshot, &state.changed_paths); + state.changed_paths.clear(); + + self.status_updates_tx + .unbounded_send(ScanState::Updated { + snapshot: new_snapshot, + changes, + scanning, + barrier, + }) + .is_ok() + } + + async fn scan_dir(&self, job: &ScanJob) -> Result<()> { + log::debug!("scan directory {:?}", job.path); + + let mut ignore_stack = job.ignore_stack.clone(); + let mut new_ignore = None; + let (root_abs_path, root_char_bag, next_entry_id) = { + let snapshot = &self.state.lock().snapshot; + ( + snapshot.abs_path().clone(), + snapshot.root_char_bag, + self.next_entry_id.clone(), + ) + }; + + let mut dotgit_path = None; + let mut root_canonical_path = None; + let mut new_entries: Vec = Vec::new(); + let mut new_jobs: Vec> = Vec::new(); + let mut child_paths = self.fs.read_dir(&job.abs_path).await?; + while let Some(child_abs_path) = child_paths.next().await { + let child_abs_path: Arc = match child_abs_path { + Ok(child_abs_path) => child_abs_path.into(), + Err(error) => { + log::error!("error processing entry {:?}", error); + continue; + } + }; + + let child_name = child_abs_path.file_name().unwrap(); + let child_path: Arc = job.path.join(child_name).into(); + let child_metadata = match self.fs.metadata(&child_abs_path).await { + Ok(Some(metadata)) => metadata, + Ok(None) => continue, + Err(err) => { + log::error!("error processing {:?}: {:?}", child_abs_path, err); + continue; + } + }; + + // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored + if child_name == *GITIGNORE { + match build_gitignore(&child_abs_path, self.fs.as_ref()).await { + Ok(ignore) => { + let ignore = Arc::new(ignore); + ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); + new_ignore = Some(ignore); + } + Err(error) => { + log::error!( + "error loading .gitignore file {:?} - {:?}", + child_name, + error + ); + } + } + + // Update ignore status of any child entries we've already processed to reflect the + // ignore file in the current directory. Because `.gitignore` starts with a `.`, + // there should rarely be too numerous. Update the ignore stack associated with any + // new jobs as well. + let mut new_jobs = new_jobs.iter_mut(); + for entry in &mut new_entries { + let entry_abs_path = root_abs_path.join(&entry.path); + entry.is_ignored = + ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir()); + + if entry.is_dir() { + if let Some(job) = new_jobs.next().expect("missing scan job for entry") { + job.ignore_stack = if entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }; + } + } + } + } + // If we find a .git, we'll need to load the repository. + else if child_name == *DOT_GIT { + dotgit_path = Some(child_path.clone()); + } + + let mut child_entry = Entry::new( + child_path.clone(), + &child_metadata, + &next_entry_id, + root_char_bag, + ); + + if job.is_external { + child_entry.is_external = true; + } else if child_metadata.is_symlink { + let canonical_path = match self.fs.canonicalize(&child_abs_path).await { + Ok(path) => path, + Err(err) => { + log::error!( + "error reading target of symlink {:?}: {:?}", + child_abs_path, + err + ); + continue; + } + }; + + // lazily canonicalize the root path in order to determine if + // symlinks point outside of the worktree. + let root_canonical_path = match &root_canonical_path { + Some(path) => path, + None => match self.fs.canonicalize(&root_abs_path).await { + Ok(path) => root_canonical_path.insert(path), + Err(err) => { + log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err); + continue; + } + }, + }; + + if !canonical_path.starts_with(root_canonical_path) { + child_entry.is_external = true; + } + } + + if child_entry.is_dir() { + child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); + + // Avoid recursing until crash in the case of a recursive symlink + if !job.ancestor_inodes.contains(&child_entry.inode) { + let mut ancestor_inodes = job.ancestor_inodes.clone(); + ancestor_inodes.insert(child_entry.inode); + + new_jobs.push(Some(ScanJob { + abs_path: child_abs_path, + path: child_path, + is_external: child_entry.is_external, + ignore_stack: if child_entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }, + ancestor_inodes, + scan_queue: job.scan_queue.clone(), + containing_repository: job.containing_repository.clone(), + })); + } else { + new_jobs.push(None); + } + } else { + child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); + if !child_entry.is_ignored { + if let Some((repository_dir, repository, staged_statuses)) = + &job.containing_repository + { + if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { + let repo_path = RepoPath(repo_path.into()); + child_entry.git_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repository + .lock() + .unstaged_status(&repo_path, child_entry.mtime), + ); + } + } + } + } + + new_entries.push(child_entry); + } + + let mut state = self.state.lock(); + + // Identify any subdirectories that should not be scanned. + let mut job_ix = 0; + for entry in &mut new_entries { + state.reuse_entry_id(entry); + if entry.is_dir() { + if state.should_scan_directory(&entry) { + job_ix += 1; + } else { + log::debug!("defer scanning directory {:?}", entry.path); + entry.kind = EntryKind::UnloadedDir; + new_jobs.remove(job_ix); + } + } + } + + state.populate_dir(&job.path, new_entries, new_ignore); + + let repository = + dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref())); + + for new_job in new_jobs { + if let Some(mut new_job) = new_job { + if let Some(containing_repository) = &repository { + new_job.containing_repository = Some(containing_repository.clone()); + } + + job.scan_queue + .try_send(new_job) + .expect("channel is unbounded"); + } + } + + Ok(()) + } + + async fn reload_entries_for_paths( + &self, + root_abs_path: Arc, + root_canonical_path: PathBuf, + relative_paths: &[Arc], + abs_paths: Vec, + scan_queue_tx: Option>, + ) { + let metadata = futures::future::join_all( + abs_paths + .iter() + .map(|abs_path| async move { + let metadata = self.fs.metadata(&abs_path).await?; + if let Some(metadata) = metadata { + let canonical_path = self.fs.canonicalize(&abs_path).await?; + anyhow::Ok(Some((metadata, canonical_path))) + } else { + Ok(None) + } + }) + .collect::>(), + ) + .await; + + let mut state = self.state.lock(); + let snapshot = &mut state.snapshot; + let is_idle = snapshot.completed_scan_id == snapshot.scan_id; + let doing_recursive_update = scan_queue_tx.is_some(); + snapshot.scan_id += 1; + if is_idle && !doing_recursive_update { + snapshot.completed_scan_id = snapshot.scan_id; + } + + // Remove any entries for paths that no longer exist or are being recursively + // refreshed. Do this before adding any new entries, so that renames can be + // detected regardless of the order of the paths. + for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { + if matches!(metadata, Ok(None)) || doing_recursive_update { + log::trace!("remove path {:?}", path); + state.remove_path(path); + } + } + + for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { + let abs_path: Arc = root_abs_path.join(&path).into(); + match metadata { + Ok(Some((metadata, canonical_path))) => { + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(&abs_path, metadata.is_dir); + + let mut fs_entry = Entry::new( + path.clone(), + metadata, + self.next_entry_id.as_ref(), + state.snapshot.root_char_bag, + ); + fs_entry.is_ignored = ignore_stack.is_all(); + fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path); + + if !fs_entry.is_ignored { + if !fs_entry.is_dir() { + if let Some((work_dir, repo)) = + state.snapshot.local_repo_for_path(&path) + { + if let Ok(repo_path) = path.strip_prefix(work_dir.0) { + let repo_path = RepoPath(repo_path.into()); + let repo = repo.repo_ptr.lock(); + fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime); + } + } + } + } + + if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) { + if state.should_scan_directory(&fs_entry) { + state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx); + } else { + fs_entry.kind = EntryKind::UnloadedDir; + } + } + + state.insert_entry(fs_entry, self.fs.as_ref()); + } + Ok(None) => { + self.remove_repo_path(&path, &mut state.snapshot); + } + Err(err) => { + // TODO - create a special 'error' entry in the entries tree to mark this + log::error!("error reading file on event {:?}", err); + } + } + } + + util::extend_sorted( + &mut state.changed_paths, + relative_paths.iter().cloned(), + usize::MAX, + Ord::cmp, + ); + } + + fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { + if !path + .components() + .any(|component| component.as_os_str() == *DOT_GIT) + { + if let Some(repository) = snapshot.repository_for_work_directory(path) { + let entry = repository.work_directory.0; + snapshot.git_repositories.remove(&entry); + snapshot + .snapshot + .repository_entries + .remove(&RepositoryWorkDirectory(path.into())); + return Some(()); + } + } + + // TODO statuses + // Track when a .git is removed and iterate over the file system there + + Some(()) + } + + async fn update_ignore_statuses(&self, scan_job_tx: Sender) { + use futures::FutureExt as _; + + let mut snapshot = self.state.lock().snapshot.clone(); + let mut ignores_to_update = Vec::new(); + let mut ignores_to_delete = Vec::new(); + let abs_path = snapshot.abs_path.clone(); + for (parent_abs_path, (_, needs_update)) in &mut snapshot.ignores_by_parent_abs_path { + if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) { + if *needs_update { + *needs_update = false; + if snapshot.snapshot.entry_for_path(parent_path).is_some() { + ignores_to_update.push(parent_abs_path.clone()); + } + } + + let ignore_path = parent_path.join(&*GITIGNORE); + if snapshot.snapshot.entry_for_path(ignore_path).is_none() { + ignores_to_delete.push(parent_abs_path.clone()); + } + } + } + + for parent_abs_path in ignores_to_delete { + snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path); + self.state + .lock() + .snapshot + .ignores_by_parent_abs_path + .remove(&parent_abs_path); + } + + let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); + ignores_to_update.sort_unstable(); + let mut ignores_to_update = ignores_to_update.into_iter().peekable(); + while let Some(parent_abs_path) = ignores_to_update.next() { + while ignores_to_update + .peek() + .map_or(false, |p| p.starts_with(&parent_abs_path)) + { + ignores_to_update.next().unwrap(); + } + + let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true); + smol::block_on(ignore_queue_tx.send(UpdateIgnoreStatusJob { + abs_path: parent_abs_path, + ignore_stack, + ignore_queue: ignore_queue_tx.clone(), + scan_queue: scan_job_tx.clone(), + })) + .unwrap(); + } + drop(ignore_queue_tx); + + self.executor + .scoped(|scope| { + for _ in 0..self.executor.num_cpus() { + scope.spawn(async { + loop { + select_biased! { + // Process any path refresh requests before moving on to process + // the queue of ignore statuses. + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, true).await { + return; + } + } + + // Recursively process directories whose ignores have changed. + job = ignore_queue_rx.recv().fuse() => { + let Ok(job) = job else { break }; + self.update_ignore_status(job, &snapshot).await; + } + } + } + }); + } + }) + .await; + } + + async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { + log::trace!("update ignore status {:?}", job.abs_path); + + let mut ignore_stack = job.ignore_stack; + if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { + ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); + } + + let mut entries_by_id_edits = Vec::new(); + let mut entries_by_path_edits = Vec::new(); + let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap(); + for mut entry in snapshot.child_entries(path).cloned() { + let was_ignored = entry.is_ignored; + let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); + entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir()); + if entry.is_dir() { + let child_ignore_stack = if entry.is_ignored { + IgnoreStack::all() + } else { + ignore_stack.clone() + }; + + // Scan any directories that were previously ignored and weren't + // previously scanned. + if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { + let state = self.state.lock(); + if state.should_scan_directory(&entry) { + state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue); + } + } + + job.ignore_queue + .send(UpdateIgnoreStatusJob { + abs_path: abs_path.clone(), + ignore_stack: child_ignore_stack, + ignore_queue: job.ignore_queue.clone(), + scan_queue: job.scan_queue.clone(), + }) + .await + .unwrap(); + } + + if entry.is_ignored != was_ignored { + let mut path_entry = snapshot.entries_by_id.get(&entry.id, &()).unwrap().clone(); + path_entry.scan_id = snapshot.scan_id; + path_entry.is_ignored = entry.is_ignored; + entries_by_id_edits.push(Edit::Insert(path_entry)); + entries_by_path_edits.push(Edit::Insert(entry)); + } + } + + let state = &mut self.state.lock(); + for edit in &entries_by_path_edits { + if let Edit::Insert(entry) = edit { + if let Err(ix) = state.changed_paths.binary_search(&entry.path) { + state.changed_paths.insert(ix, entry.path.clone()); + } + } + } + + state + .snapshot + .entries_by_path + .edit(entries_by_path_edits, &()); + state.snapshot.entries_by_id.edit(entries_by_id_edits, &()); + } + + fn build_change_set( + &self, + old_snapshot: &Snapshot, + new_snapshot: &Snapshot, + event_paths: &[Arc], + ) -> UpdatedEntriesSet { + use BackgroundScannerPhase::*; + use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated}; + + // Identify which paths have changed. Use the known set of changed + // parent paths to optimize the search. + let mut changes = Vec::new(); + let mut old_paths = old_snapshot.entries_by_path.cursor::(); + let mut new_paths = new_snapshot.entries_by_path.cursor::(); + let mut last_newly_loaded_dir_path = None; + old_paths.next(&()); + new_paths.next(&()); + for path in event_paths { + let path = PathKey(path.clone()); + if old_paths.item().map_or(false, |e| e.path < path.0) { + old_paths.seek_forward(&path, Bias::Left, &()); + } + if new_paths.item().map_or(false, |e| e.path < path.0) { + new_paths.seek_forward(&path, Bias::Left, &()); + } + loop { + match (old_paths.item(), new_paths.item()) { + (Some(old_entry), Some(new_entry)) => { + if old_entry.path > path.0 + && new_entry.path > path.0 + && !old_entry.path.starts_with(&path.0) + && !new_entry.path.starts_with(&path.0) + { + break; + } + + match Ord::cmp(&old_entry.path, &new_entry.path) { + Ordering::Less => { + changes.push((old_entry.path.clone(), old_entry.id, Removed)); + old_paths.next(&()); + } + Ordering::Equal => { + if self.phase == EventsReceivedDuringInitialScan { + if old_entry.id != new_entry.id { + changes.push(( + old_entry.path.clone(), + old_entry.id, + Removed, + )); + } + // If the worktree was not fully initialized when this event was generated, + // we can't know whether this entry was added during the scan or whether + // it was merely updated. + changes.push(( + new_entry.path.clone(), + new_entry.id, + AddedOrUpdated, + )); + } else if old_entry.id != new_entry.id { + changes.push((old_entry.path.clone(), old_entry.id, Removed)); + changes.push((new_entry.path.clone(), new_entry.id, Added)); + } else if old_entry != new_entry { + if old_entry.kind.is_unloaded() { + last_newly_loaded_dir_path = Some(&new_entry.path); + changes.push(( + new_entry.path.clone(), + new_entry.id, + Loaded, + )); + } else { + changes.push(( + new_entry.path.clone(), + new_entry.id, + Updated, + )); + } + } + old_paths.next(&()); + new_paths.next(&()); + } + Ordering::Greater => { + let is_newly_loaded = self.phase == InitialScan + || last_newly_loaded_dir_path + .as_ref() + .map_or(false, |dir| new_entry.path.starts_with(&dir)); + changes.push(( + new_entry.path.clone(), + new_entry.id, + if is_newly_loaded { Loaded } else { Added }, + )); + new_paths.next(&()); + } + } + } + (Some(old_entry), None) => { + changes.push((old_entry.path.clone(), old_entry.id, Removed)); + old_paths.next(&()); + } + (None, Some(new_entry)) => { + let is_newly_loaded = self.phase == InitialScan + || last_newly_loaded_dir_path + .as_ref() + .map_or(false, |dir| new_entry.path.starts_with(&dir)); + changes.push(( + new_entry.path.clone(), + new_entry.id, + if is_newly_loaded { Loaded } else { Added }, + )); + new_paths.next(&()); + } + (None, None) => break, + } + } + } + + changes.into() + } + + async fn progress_timer(&self, running: bool) { + if !running { + return futures::future::pending().await; + } + + #[cfg(any(test, feature = "test-support"))] + if self.fs.is_fake() { + return self.executor.simulate_random_delay().await; + } + + smol::Timer::after(Duration::from_millis(100)).await; + } +} + +fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { + let mut result = root_char_bag; + result.extend( + path.to_string_lossy() + .chars() + .map(|c| c.to_ascii_lowercase()), + ); + result +} + +struct ScanJob { + abs_path: Arc, + path: Arc, + ignore_stack: Arc, + scan_queue: Sender, + ancestor_inodes: TreeSet, + is_external: bool, + containing_repository: Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )>, +} + +struct UpdateIgnoreStatusJob { + abs_path: Arc, + ignore_stack: Arc, + ignore_queue: Sender, + scan_queue: Sender, +} + +// todo!("re-enable when we have tests") +// pub trait WorktreeModelHandle { +// #[cfg(any(test, feature = "test-support"))] +// fn flush_fs_events<'a>( +// &self, +// cx: &'a gpui::TestAppContext, +// ) -> futures::future::LocalBoxFuture<'a, ()>; +// } + +// impl WorktreeModelHandle for Handle { +// // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that +// // occurred before the worktree was constructed. These events can cause the worktree to perform +// // extra directory scans, and emit extra scan-state notifications. +// // +// // This function mutates the worktree's directory and waits for those mutations to be picked up, +// // to ensure that all redundant FS events have already been processed. +// #[cfg(any(test, feature = "test-support"))] +// fn flush_fs_events<'a>( +// &self, +// cx: &'a gpui::TestAppContext, +// ) -> futures::future::LocalBoxFuture<'a, ()> { +// let filename = "fs-event-sentinel"; +// let tree = self.clone(); +// let (fs, root_path) = self.read_with(cx, |tree, _| { +// let tree = tree.as_local().unwrap(); +// (tree.fs.clone(), tree.abs_path().clone()) +// }); + +// async move { +// fs.create_file(&root_path.join(filename), Default::default()) +// .await +// .unwrap(); +// tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_some()) +// .await; + +// fs.remove_file(&root_path.join(filename), Default::default()) +// .await +// .unwrap(); +// tree.condition(cx, |tree, _| tree.entry_for_path(filename).is_none()) +// .await; + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; +// } +// .boxed_local() +// } +// } + +#[derive(Clone, Debug)] +struct TraversalProgress<'a> { + max_path: &'a Path, + count: usize, + non_ignored_count: usize, + file_count: usize, + non_ignored_file_count: usize, +} + +impl<'a> TraversalProgress<'a> { + fn count(&self, include_dirs: bool, include_ignored: bool) -> usize { + match (include_ignored, include_dirs) { + (true, true) => self.count, + (true, false) => self.file_count, + (false, true) => self.non_ignored_count, + (false, false) => self.non_ignored_file_count, + } + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + self.max_path = summary.max_path.as_ref(); + self.count += summary.count; + self.non_ignored_count += summary.non_ignored_count; + self.file_count += summary.file_count; + self.non_ignored_file_count += summary.non_ignored_file_count; + } +} + +impl<'a> Default for TraversalProgress<'a> { + fn default() -> Self { + Self { + max_path: Path::new(""), + count: 0, + non_ignored_count: 0, + file_count: 0, + non_ignored_file_count: 0, + } + } +} + +#[derive(Clone, Debug, Default, Copy)] +struct GitStatuses { + added: usize, + modified: usize, + conflict: usize, +} + +impl AddAssign for GitStatuses { + fn add_assign(&mut self, rhs: Self) { + self.added += rhs.added; + self.modified += rhs.modified; + self.conflict += rhs.conflict; + } +} + +impl Sub for GitStatuses { + type Output = GitStatuses; + + fn sub(self, rhs: Self) -> Self::Output { + GitStatuses { + added: self.added - rhs.added, + modified: self.modified - rhs.modified, + conflict: self.conflict - rhs.conflict, + } + } +} + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + *self += summary.statuses + } +} + +pub struct Traversal<'a> { + cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, + include_ignored: bool, + include_dirs: bool, +} + +impl<'a> Traversal<'a> { + pub fn advance(&mut self) -> bool { + self.cursor.seek_forward( + &TraversalTarget::Count { + count: self.end_offset() + 1, + include_dirs: self.include_dirs, + include_ignored: self.include_ignored, + }, + Bias::Left, + &(), + ) + } + + pub fn advance_to_sibling(&mut self) -> bool { + while let Some(entry) = self.cursor.item() { + self.cursor.seek_forward( + &TraversalTarget::PathSuccessor(&entry.path), + Bias::Left, + &(), + ); + if let Some(entry) = self.cursor.item() { + if (self.include_dirs || !entry.is_dir()) + && (self.include_ignored || !entry.is_ignored) + { + return true; + } + } + } + false + } + + pub fn entry(&self) -> Option<&'a Entry> { + self.cursor.item() + } + + pub fn start_offset(&self) -> usize { + self.cursor + .start() + .count(self.include_dirs, self.include_ignored) + } + + pub fn end_offset(&self) -> usize { + self.cursor + .end(&()) + .count(self.include_dirs, self.include_ignored) + } +} + +impl<'a> Iterator for Traversal<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.entry() { + self.advance(); + Some(item) + } else { + None + } + } +} + +#[derive(Debug)] +enum TraversalTarget<'a> { + Path(&'a Path), + PathSuccessor(&'a Path), + Count { + count: usize, + include_ignored: bool, + include_dirs: bool, + }, +} + +impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'b> { + fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { + match self { + TraversalTarget::Path(path) => path.cmp(&cursor_location.max_path), + TraversalTarget::PathSuccessor(path) => { + if !cursor_location.max_path.starts_with(path) { + Ordering::Equal + } else { + Ordering::Greater + } + } + TraversalTarget::Count { + count, + include_dirs, + include_ignored, + } => Ord::cmp( + count, + &cursor_location.count(*include_dirs, *include_ignored), + ), + } + } +} + +impl<'a, 'b> SeekTarget<'a, EntrySummary, (TraversalProgress<'a>, GitStatuses)> + for TraversalTarget<'b> +{ + fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitStatuses), _: &()) -> Ordering { + self.cmp(&cursor_location.0, &()) + } +} + +struct ChildEntriesIter<'a> { + parent_path: &'a Path, + traversal: Traversal<'a>, +} + +impl<'a> Iterator for ChildEntriesIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(&self.parent_path) { + self.traversal.advance_to_sibling(); + return Some(item); + } + } + None + } +} + +pub struct DescendentEntriesIter<'a> { + parent_path: &'a Path, + traversal: Traversal<'a>, +} + +impl<'a> Iterator for DescendentEntriesIter<'a> { + type Item = &'a Entry; + + fn next(&mut self) -> Option { + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(&self.parent_path) { + self.traversal.advance(); + return Some(item); + } + } + None + } +} + +impl<'a> From<&'a Entry> for proto::Entry { + fn from(entry: &'a Entry) -> Self { + Self { + id: entry.id.to_proto(), + is_dir: entry.is_dir(), + path: entry.path.to_string_lossy().into(), + inode: entry.inode, + mtime: Some(entry.mtime.into()), + is_symlink: entry.is_symlink, + is_ignored: entry.is_ignored, + is_external: entry.is_external, + git_status: entry.git_status.map(git_status_to_proto), + } + } +} + +impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { + type Error = anyhow::Error; + + fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result { + if let Some(mtime) = entry.mtime { + let kind = if entry.is_dir { + EntryKind::Dir + } else { + let mut char_bag = *root_char_bag; + char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase())); + EntryKind::File(char_bag) + }; + let path: Arc = PathBuf::from(entry.path).into(); + Ok(Entry { + id: ProjectEntryId::from_proto(entry.id), + kind, + path, + inode: entry.inode, + mtime: mtime.into(), + is_symlink: entry.is_symlink, + is_ignored: entry.is_ignored, + is_external: entry.is_external, + git_status: git_status_from_proto(entry.git_status), + }) + } else { + Err(anyhow!( + "missing mtime in remote worktree entry {:?}", + entry.path + )) + } + } +} + +fn combine_git_statuses( + staged: Option, + unstaged: Option, +) -> Option { + if let Some(staged) = staged { + if let Some(unstaged) = unstaged { + if unstaged != staged { + Some(GitFileStatus::Modified) + } else { + Some(staged) + } + } else { + Some(staged) + } + } else { + unstaged + } +} + +fn git_status_from_proto(git_status: Option) -> Option { + git_status.and_then(|status| { + proto::GitStatus::from_i32(status).map(|status| match status { + proto::GitStatus::Added => GitFileStatus::Added, + proto::GitStatus::Modified => GitFileStatus::Modified, + proto::GitStatus::Conflict => GitFileStatus::Conflict, + }) + }) +} + +fn git_status_to_proto(status: GitFileStatus) -> i32 { + match status { + GitFileStatus::Added => proto::GitStatus::Added as i32, + GitFileStatus::Modified => proto::GitStatus::Modified as i32, + GitFileStatus::Conflict => proto::GitStatus::Conflict as i32, + } +} diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs new file mode 100644 index 0000000000..bf195f24c4 --- /dev/null +++ b/crates/project2/src/worktree_tests.rs @@ -0,0 +1,2141 @@ +// use crate::{ +// worktree::{Event, Snapshot, WorktreeModelHandle}, +// Entry, EntryKind, PathChange, Worktree, +// }; +// use anyhow::Result; +// use client2::Client; +// use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions}; +// use git::GITIGNORE; +// use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext}; +// use parking_lot::Mutex; +// use postage::stream::Stream; +// use pretty_assertions::assert_eq; +// use rand::prelude::*; +// use serde_json::json; +// use std::{ +// env, +// fmt::Write, +// mem, +// path::{Path, PathBuf}, +// sync::Arc, +// }; +// use util::{http::FakeHttpClient, test::temp_tree, ResultExt}; + +// #[gpui::test] +// async fn test_traversal(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// ".gitignore": "a/b\n", +// "a": { +// "b": "", +// "c": "", +// } +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs, +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(false) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![ +// Path::new(""), +// Path::new(".gitignore"), +// Path::new("a"), +// Path::new("a/c"), +// ] +// ); +// assert_eq!( +// tree.entries(true) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![ +// Path::new(""), +// Path::new(".gitignore"), +// Path::new("a"), +// Path::new("a/b"), +// Path::new("a/c"), +// ] +// ); +// }) +// } + +// #[gpui::test] +// async fn test_descendent_entries(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// "a": "", +// "b": { +// "c": { +// "d": "" +// }, +// "e": {} +// }, +// "f": "", +// "g": { +// "h": {} +// }, +// "i": { +// "j": { +// "k": "" +// }, +// "l": { + +// } +// }, +// ".gitignore": "i/j\n", +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs, +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.descendent_entries(false, false, Path::new("b")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![Path::new("b/c/d"),] +// ); +// assert_eq!( +// tree.descendent_entries(true, false, Path::new("b")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![ +// Path::new("b"), +// Path::new("b/c"), +// Path::new("b/c/d"), +// Path::new("b/e"), +// ] +// ); + +// assert_eq!( +// tree.descendent_entries(false, false, Path::new("g")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// Vec::::new() +// ); +// assert_eq!( +// tree.descendent_entries(true, false, Path::new("g")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![Path::new("g"), Path::new("g/h"),] +// ); +// }); + +// // Expand gitignored directory. +// tree.read_with(cx, |tree, _| { +// tree.as_local() +// .unwrap() +// .refresh_entries_for_paths(vec![Path::new("i/j").into()]) +// }) +// .recv() +// .await; + +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.descendent_entries(false, false, Path::new("i")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// Vec::::new() +// ); +// assert_eq!( +// tree.descendent_entries(false, true, Path::new("i")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![Path::new("i/j/k")] +// ); +// assert_eq!( +// tree.descendent_entries(true, false, Path::new("i")) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![Path::new("i"), Path::new("i/l"),] +// ); +// }) +// } + +// #[gpui::test(iterations = 10)] +// async fn test_circular_symlinks(executor: Arc, cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// "lib": { +// "a": { +// "a.txt": "" +// }, +// "b": { +// "b.txt": "" +// } +// } +// }), +// ) +// .await; +// fs.insert_symlink("/root/lib/a/lib", "..".into()).await; +// fs.insert_symlink("/root/lib/b/lib", "..".into()).await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(false) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![ +// Path::new(""), +// Path::new("lib"), +// Path::new("lib/a"), +// Path::new("lib/a/a.txt"), +// Path::new("lib/a/lib"), +// Path::new("lib/b"), +// Path::new("lib/b/b.txt"), +// Path::new("lib/b/lib"), +// ] +// ); +// }); + +// fs.rename( +// Path::new("/root/lib/a/lib"), +// Path::new("/root/lib/a/lib-2"), +// Default::default(), +// ) +// .await +// .unwrap(); +// executor.run_until_parked(); +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(false) +// .map(|entry| entry.path.as_ref()) +// .collect::>(), +// vec![ +// Path::new(""), +// Path::new("lib"), +// Path::new("lib/a"), +// Path::new("lib/a/a.txt"), +// Path::new("lib/a/lib-2"), +// Path::new("lib/b"), +// Path::new("lib/b/b.txt"), +// Path::new("lib/b/lib"), +// ] +// ); +// }); +// } + +// #[gpui::test] +// async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// "dir1": { +// "deps": { +// // symlinks here +// }, +// "src": { +// "a.rs": "", +// "b.rs": "", +// }, +// }, +// "dir2": { +// "src": { +// "c.rs": "", +// "d.rs": "", +// } +// }, +// "dir3": { +// "deps": {}, +// "src": { +// "e.rs": "", +// "f.rs": "", +// }, +// } +// }), +// ) +// .await; + +// // These symlinks point to directories outside of the worktree's root, dir1. +// fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into()) +// .await; +// fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into()) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root/dir1"), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// let tree_updates = Arc::new(Mutex::new(Vec::new())); +// tree.update(cx, |_, cx| { +// let tree_updates = tree_updates.clone(); +// cx.subscribe(&tree, move |_, _, event, _| { +// if let Event::UpdatedEntries(update) = event { +// tree_updates.lock().extend( +// update +// .iter() +// .map(|(path, _, change)| (path.clone(), *change)), +// ); +// } +// }) +// .detach(); +// }); + +// // The symlinked directories are not scanned by default. +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_external)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new("deps"), false), +// (Path::new("deps/dep-dir2"), true), +// (Path::new("deps/dep-dir3"), true), +// (Path::new("src"), false), +// (Path::new("src/a.rs"), false), +// (Path::new("src/b.rs"), false), +// ] +// ); + +// assert_eq!( +// tree.entry_for_path("deps/dep-dir2").unwrap().kind, +// EntryKind::UnloadedDir +// ); +// }); + +// // Expand one of the symlinked directories. +// tree.read_with(cx, |tree, _| { +// tree.as_local() +// .unwrap() +// .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()]) +// }) +// .recv() +// .await; + +// // The expanded directory's contents are loaded. Subdirectories are +// // not scanned yet. +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_external)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new("deps"), false), +// (Path::new("deps/dep-dir2"), true), +// (Path::new("deps/dep-dir3"), true), +// (Path::new("deps/dep-dir3/deps"), true), +// (Path::new("deps/dep-dir3/src"), true), +// (Path::new("src"), false), +// (Path::new("src/a.rs"), false), +// (Path::new("src/b.rs"), false), +// ] +// ); +// }); +// assert_eq!( +// mem::take(&mut *tree_updates.lock()), +// &[ +// (Path::new("deps/dep-dir3").into(), PathChange::Loaded), +// (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded), +// (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded) +// ] +// ); + +// // Expand a subdirectory of one of the symlinked directories. +// tree.read_with(cx, |tree, _| { +// tree.as_local() +// .unwrap() +// .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()]) +// }) +// .recv() +// .await; + +// // The expanded subdirectory's contents are loaded. +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_external)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new("deps"), false), +// (Path::new("deps/dep-dir2"), true), +// (Path::new("deps/dep-dir3"), true), +// (Path::new("deps/dep-dir3/deps"), true), +// (Path::new("deps/dep-dir3/src"), true), +// (Path::new("deps/dep-dir3/src/e.rs"), true), +// (Path::new("deps/dep-dir3/src/f.rs"), true), +// (Path::new("src"), false), +// (Path::new("src/a.rs"), false), +// (Path::new("src/b.rs"), false), +// ] +// ); +// }); + +// assert_eq!( +// mem::take(&mut *tree_updates.lock()), +// &[ +// (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded), +// ( +// Path::new("deps/dep-dir3/src/e.rs").into(), +// PathChange::Loaded +// ), +// ( +// Path::new("deps/dep-dir3/src/f.rs").into(), +// PathChange::Loaded +// ) +// ] +// ); +// } + +// #[gpui::test] +// async fn test_open_gitignored_files(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// ".gitignore": "node_modules\n", +// "one": { +// "node_modules": { +// "a": { +// "a1.js": "a1", +// "a2.js": "a2", +// }, +// "b": { +// "b1.js": "b1", +// "b2.js": "b2", +// }, +// "c": { +// "c1.js": "c1", +// "c2.js": "c2", +// } +// }, +// }, +// "two": { +// "x.js": "", +// "y.js": "", +// }, +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("one"), false), +// (Path::new("one/node_modules"), true), +// (Path::new("two"), false), +// (Path::new("two/x.js"), false), +// (Path::new("two/y.js"), false), +// ] +// ); +// }); + +// // Open a file that is nested inside of a gitignored directory that +// // has not yet been expanded. +// let prev_read_dir_count = fs.read_dir_call_count(); +// let buffer = tree +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx) +// }) +// .await +// .unwrap(); + +// tree.read_with(cx, |tree, cx| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("one"), false), +// (Path::new("one/node_modules"), true), +// (Path::new("one/node_modules/a"), true), +// (Path::new("one/node_modules/b"), true), +// (Path::new("one/node_modules/b/b1.js"), true), +// (Path::new("one/node_modules/b/b2.js"), true), +// (Path::new("one/node_modules/c"), true), +// (Path::new("two"), false), +// (Path::new("two/x.js"), false), +// (Path::new("two/y.js"), false), +// ] +// ); + +// assert_eq!( +// buffer.read(cx).file().unwrap().path().as_ref(), +// Path::new("one/node_modules/b/b1.js") +// ); + +// // Only the newly-expanded directories are scanned. +// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); +// }); + +// // Open another file in a different subdirectory of the same +// // gitignored directory. +// let prev_read_dir_count = fs.read_dir_call_count(); +// let buffer = tree +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx) +// }) +// .await +// .unwrap(); + +// tree.read_with(cx, |tree, cx| { +// assert_eq!( +// tree.entries(true) +// .map(|entry| (entry.path.as_ref(), entry.is_ignored)) +// .collect::>(), +// vec![ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("one"), false), +// (Path::new("one/node_modules"), true), +// (Path::new("one/node_modules/a"), true), +// (Path::new("one/node_modules/a/a1.js"), true), +// (Path::new("one/node_modules/a/a2.js"), true), +// (Path::new("one/node_modules/b"), true), +// (Path::new("one/node_modules/b/b1.js"), true), +// (Path::new("one/node_modules/b/b2.js"), true), +// (Path::new("one/node_modules/c"), true), +// (Path::new("two"), false), +// (Path::new("two/x.js"), false), +// (Path::new("two/y.js"), false), +// ] +// ); + +// assert_eq!( +// buffer.read(cx).file().unwrap().path().as_ref(), +// Path::new("one/node_modules/a/a2.js") +// ); + +// // Only the newly-expanded directory is scanned. +// assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); +// }); + +// // No work happens when files and directories change within an unloaded directory. +// let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count(); +// fs.create_dir("/root/one/node_modules/c/lib".as_ref()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); +// assert_eq!( +// fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count, +// 0 +// ); +// } + +// #[gpui::test] +// async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// ".gitignore": "node_modules\n", +// "a": { +// "a.js": "", +// }, +// "b": { +// "b.js": "", +// }, +// "node_modules": { +// "c": { +// "c.js": "", +// }, +// "d": { +// "d.js": "", +// "e": { +// "e1.js": "", +// "e2.js": "", +// }, +// "f": { +// "f1.js": "", +// "f2.js": "", +// } +// }, +// }, +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// // Open a file within the gitignored directory, forcing some of its +// // subdirectories to be read, but not all. +// let read_dir_count_1 = fs.read_dir_call_count(); +// tree.read_with(cx, |tree, _| { +// tree.as_local() +// .unwrap() +// .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()]) +// }) +// .recv() +// .await; + +// // Those subdirectories are now loaded. +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|e| (e.path.as_ref(), e.is_ignored)) +// .collect::>(), +// &[ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("a"), false), +// (Path::new("a/a.js"), false), +// (Path::new("b"), false), +// (Path::new("b/b.js"), false), +// (Path::new("node_modules"), true), +// (Path::new("node_modules/c"), true), +// (Path::new("node_modules/d"), true), +// (Path::new("node_modules/d/d.js"), true), +// (Path::new("node_modules/d/e"), true), +// (Path::new("node_modules/d/f"), true), +// ] +// ); +// }); +// let read_dir_count_2 = fs.read_dir_call_count(); +// assert_eq!(read_dir_count_2 - read_dir_count_1, 2); + +// // Update the gitignore so that node_modules is no longer ignored, +// // but a subdirectory is ignored +// fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) +// .await +// .unwrap(); +// cx.foreground().run_until_parked(); + +// // All of the directories that are no longer ignored are now loaded. +// tree.read_with(cx, |tree, _| { +// assert_eq!( +// tree.entries(true) +// .map(|e| (e.path.as_ref(), e.is_ignored)) +// .collect::>(), +// &[ +// (Path::new(""), false), +// (Path::new(".gitignore"), false), +// (Path::new("a"), false), +// (Path::new("a/a.js"), false), +// (Path::new("b"), false), +// (Path::new("b/b.js"), false), +// // This directory is no longer ignored +// (Path::new("node_modules"), false), +// (Path::new("node_modules/c"), false), +// (Path::new("node_modules/c/c.js"), false), +// (Path::new("node_modules/d"), false), +// (Path::new("node_modules/d/d.js"), false), +// // This subdirectory is now ignored +// (Path::new("node_modules/d/e"), true), +// (Path::new("node_modules/d/f"), false), +// (Path::new("node_modules/d/f/f1.js"), false), +// (Path::new("node_modules/d/f/f2.js"), false), +// ] +// ); +// }); + +// // Each of the newly-loaded directories is scanned only once. +// let read_dir_count_3 = fs.read_dir_call_count(); +// assert_eq!(read_dir_count_3 - read_dir_count_2, 2); +// } + +// #[gpui::test(iterations = 10)] +// async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", +// "tree": { +// ".git": {}, +// ".gitignore": "ignored-dir\n", +// "tracked-dir": { +// "tracked-file1": "", +// "ancestor-ignored-file1": "", +// }, +// "ignored-dir": { +// "ignored-file1": "" +// } +// } +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// "/root/tree".as_ref(), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.read_with(cx, |tree, _| { +// tree.as_local() +// .unwrap() +// .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()]) +// }) +// .recv() +// .await; + +// cx.read(|cx| { +// let tree = tree.read(cx); +// assert!( +// !tree +// .entry_for_path("tracked-dir/tracked-file1") +// .unwrap() +// .is_ignored +// ); +// assert!( +// tree.entry_for_path("tracked-dir/ancestor-ignored-file1") +// .unwrap() +// .is_ignored +// ); +// assert!( +// tree.entry_for_path("ignored-dir/ignored-file1") +// .unwrap() +// .is_ignored +// ); +// }); + +// fs.create_file( +// "/root/tree/tracked-dir/tracked-file2".as_ref(), +// Default::default(), +// ) +// .await +// .unwrap(); +// fs.create_file( +// "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(), +// Default::default(), +// ) +// .await +// .unwrap(); +// fs.create_file( +// "/root/tree/ignored-dir/ignored-file2".as_ref(), +// Default::default(), +// ) +// .await +// .unwrap(); + +// cx.foreground().run_until_parked(); +// cx.read(|cx| { +// let tree = tree.read(cx); +// assert!( +// !tree +// .entry_for_path("tracked-dir/tracked-file2") +// .unwrap() +// .is_ignored +// ); +// assert!( +// tree.entry_for_path("tracked-dir/ancestor-ignored-file2") +// .unwrap() +// .is_ignored +// ); +// assert!( +// tree.entry_for_path("ignored-dir/ignored-file2") +// .unwrap() +// .is_ignored +// ); +// assert!(tree.entry_for_path(".git").unwrap().is_ignored); +// }); +// } + +// #[gpui::test] +// async fn test_write_file(cx: &mut TestAppContext) { +// let dir = temp_tree(json!({ +// ".git": {}, +// ".gitignore": "ignored-dir\n", +// "tracked-dir": {}, +// "ignored-dir": {} +// })); + +// 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.update(cx, |tree, cx| { +// tree.as_local().unwrap().write_file( +// Path::new("tracked-dir/file.txt"), +// "hello".into(), +// Default::default(), +// cx, +// ) +// }) +// .await +// .unwrap(); +// tree.update(cx, |tree, cx| { +// tree.as_local().unwrap().write_file( +// Path::new("ignored-dir/file.txt"), +// "world".into(), +// Default::default(), +// cx, +// ) +// }) +// .await +// .unwrap(); + +// tree.read_with(cx, |tree, _| { +// let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap(); +// let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap(); +// assert!(!tracked.is_ignored); +// assert!(ignored.is_ignored); +// }); +// } + +// #[gpui::test(iterations = 30)] +// async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// "b": {}, +// "c": {}, +// "d": {}, +// }), +// ) +// .await; + +// let tree = Worktree::local( +// build_client(cx), +// "/root".as_ref(), +// true, +// fs, +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let snapshot1 = tree.update(cx, |tree, cx| { +// let tree = tree.as_local_mut().unwrap(); +// let snapshot = Arc::new(Mutex::new(tree.snapshot())); +// let _ = tree.observe_updates(0, cx, { +// let snapshot = snapshot.clone(); +// move |update| { +// snapshot.lock().apply_remote_update(update).unwrap(); +// async { true } +// } +// }); +// snapshot +// }); + +// let entry = tree +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .create_entry("a/e".as_ref(), true, cx) +// }) +// .await +// .unwrap(); +// assert!(entry.is_dir()); + +// cx.foreground().run_until_parked(); +// tree.read_with(cx, |tree, _| { +// assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir); +// }); + +// let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot()); +// assert_eq!( +// snapshot1.lock().entries(true).collect::>(), +// snapshot2.entries(true).collect::>() +// ); +// } + +// #[gpui::test] +// async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { +// let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + +// let fs_fake = FakeFs::new(cx.background()); +// fs_fake +// .insert_tree( +// "/root", +// json!({ +// "a": {}, +// }), +// ) +// .await; + +// let tree_fake = Worktree::local( +// client_fake, +// "/root".as_ref(), +// true, +// fs_fake, +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let entry = tree_fake +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .create_entry("a/b/c/d.txt".as_ref(), false, cx) +// }) +// .await +// .unwrap(); +// assert!(entry.is_file()); + +// cx.foreground().run_until_parked(); +// tree_fake.read_with(cx, |tree, _| { +// assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file()); +// assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir()); +// assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); +// }); + +// let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + +// let fs_real = Arc::new(RealFs); +// let temp_root = temp_tree(json!({ +// "a": {} +// })); + +// let tree_real = Worktree::local( +// client_real, +// temp_root.path(), +// true, +// fs_real, +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let entry = tree_real +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .create_entry("a/b/c/d.txt".as_ref(), false, cx) +// }) +// .await +// .unwrap(); +// assert!(entry.is_file()); + +// cx.foreground().run_until_parked(); +// tree_real.read_with(cx, |tree, _| { +// assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file()); +// assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir()); +// assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); +// }); + +// // Test smallest change +// let entry = tree_real +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .create_entry("a/b/c/e.txt".as_ref(), false, cx) +// }) +// .await +// .unwrap(); +// assert!(entry.is_file()); + +// cx.foreground().run_until_parked(); +// tree_real.read_with(cx, |tree, _| { +// assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file()); +// }); + +// // Test largest change +// let entry = tree_real +// .update(cx, |tree, cx| { +// tree.as_local_mut() +// .unwrap() +// .create_entry("d/e/f/g.txt".as_ref(), false, cx) +// }) +// .await +// .unwrap(); +// assert!(entry.is_file()); + +// cx.foreground().run_until_parked(); +// tree_real.read_with(cx, |tree, _| { +// assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file()); +// assert!(tree.entry_for_path("d/e/f").unwrap().is_dir()); +// assert!(tree.entry_for_path("d/e/").unwrap().is_dir()); +// assert!(tree.entry_for_path("d/").unwrap().is_dir()); +// }); +// } + +// #[gpui::test(iterations = 100)] +// async fn test_random_worktree_operations_during_initial_scan( +// cx: &mut TestAppContext, +// mut rng: StdRng, +// ) { +// let operations = env::var("OPERATIONS") +// .map(|o| o.parse().unwrap()) +// .unwrap_or(5); +// let initial_entries = env::var("INITIAL_ENTRIES") +// .map(|o| o.parse().unwrap()) +// .unwrap_or(20); + +// let root_dir = Path::new("/test"); +// let fs = FakeFs::new(cx.background()) as Arc; +// fs.as_fake().insert_tree(root_dir, json!({})).await; +// for _ in 0..initial_entries { +// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; +// } +// log::info!("generated initial tree"); + +// let worktree = Worktree::local( +// build_client(cx), +// root_dir, +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())]; +// let updates = Arc::new(Mutex::new(Vec::new())); +// worktree.update(cx, |tree, cx| { +// check_worktree_change_events(tree, cx); + +// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { +// let updates = updates.clone(); +// move |update| { +// updates.lock().push(update); +// async { true } +// } +// }); +// }); + +// for _ in 0..operations { +// worktree +// .update(cx, |worktree, cx| { +// randomly_mutate_worktree(worktree, &mut rng, cx) +// }) +// .await +// .log_err(); +// worktree.read_with(cx, |tree, _| { +// tree.as_local().unwrap().snapshot().check_invariants(true) +// }); + +// if rng.gen_bool(0.6) { +// snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())); +// } +// } + +// worktree +// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) +// .await; + +// cx.foreground().run_until_parked(); + +// let final_snapshot = worktree.read_with(cx, |tree, _| { +// let tree = tree.as_local().unwrap(); +// let snapshot = tree.snapshot(); +// snapshot.check_invariants(true); +// snapshot +// }); + +// for (i, snapshot) in snapshots.into_iter().enumerate().rev() { +// let mut updated_snapshot = snapshot.clone(); +// for update in updates.lock().iter() { +// if update.scan_id >= updated_snapshot.scan_id() as u64 { +// updated_snapshot +// .apply_remote_update(update.clone()) +// .unwrap(); +// } +// } + +// assert_eq!( +// updated_snapshot.entries(true).collect::>(), +// final_snapshot.entries(true).collect::>(), +// "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}", +// ); +// } +// } + +// #[gpui::test(iterations = 100)] +// async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { +// let operations = env::var("OPERATIONS") +// .map(|o| o.parse().unwrap()) +// .unwrap_or(40); +// let initial_entries = env::var("INITIAL_ENTRIES") +// .map(|o| o.parse().unwrap()) +// .unwrap_or(20); + +// let root_dir = Path::new("/test"); +// let fs = FakeFs::new(cx.background()) as Arc; +// fs.as_fake().insert_tree(root_dir, json!({})).await; +// for _ in 0..initial_entries { +// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; +// } +// log::info!("generated initial tree"); + +// let worktree = Worktree::local( +// build_client(cx), +// root_dir, +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let updates = Arc::new(Mutex::new(Vec::new())); +// worktree.update(cx, |tree, cx| { +// check_worktree_change_events(tree, cx); + +// let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, { +// let updates = updates.clone(); +// move |update| { +// updates.lock().push(update); +// async { true } +// } +// }); +// }); + +// worktree +// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) +// .await; + +// fs.as_fake().pause_events(); +// let mut snapshots = Vec::new(); +// let mut mutations_len = operations; +// while mutations_len > 1 { +// if rng.gen_bool(0.2) { +// worktree +// .update(cx, |worktree, cx| { +// randomly_mutate_worktree(worktree, &mut rng, cx) +// }) +// .await +// .log_err(); +// } else { +// randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; +// } + +// let buffered_event_count = fs.as_fake().buffered_event_count(); +// if buffered_event_count > 0 && rng.gen_bool(0.3) { +// let len = rng.gen_range(0..=buffered_event_count); +// log::info!("flushing {} events", len); +// fs.as_fake().flush_events(len); +// } else { +// randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await; +// mutations_len -= 1; +// } + +// cx.foreground().run_until_parked(); +// if rng.gen_bool(0.2) { +// log::info!("storing snapshot {}", snapshots.len()); +// let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); +// snapshots.push(snapshot); +// } +// } + +// log::info!("quiescing"); +// fs.as_fake().flush_events(usize::MAX); +// cx.foreground().run_until_parked(); + +// let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); +// snapshot.check_invariants(true); +// let expanded_paths = snapshot +// .expanded_entries() +// .map(|e| e.path.clone()) +// .collect::>(); + +// { +// let new_worktree = Worktree::local( +// build_client(cx), +// root_dir, +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); +// new_worktree +// .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) +// .await; +// new_worktree +// .update(cx, |tree, _| { +// tree.as_local_mut() +// .unwrap() +// .refresh_entries_for_paths(expanded_paths) +// }) +// .recv() +// .await; +// let new_snapshot = +// new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); +// assert_eq!( +// snapshot.entries_without_ids(true), +// new_snapshot.entries_without_ids(true) +// ); +// } + +// for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() { +// for update in updates.lock().iter() { +// if update.scan_id >= prev_snapshot.scan_id() as u64 { +// prev_snapshot.apply_remote_update(update.clone()).unwrap(); +// } +// } + +// assert_eq!( +// prev_snapshot +// .entries(true) +// .map(ignore_pending_dir) +// .collect::>(), +// snapshot +// .entries(true) +// .map(ignore_pending_dir) +// .collect::>(), +// "wrong updates after snapshot {i}: {updates:#?}", +// ); +// } + +// fn ignore_pending_dir(entry: &Entry) -> Entry { +// let mut entry = entry.clone(); +// if entry.kind.is_dir() { +// entry.kind = EntryKind::Dir +// } +// entry +// } +// } + +// // The worktree's `UpdatedEntries` event can be used to follow along with +// // all changes to the worktree's snapshot. +// fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext) { +// let mut entries = tree.entries(true).cloned().collect::>(); +// cx.subscribe(&cx.handle(), move |tree, _, event, _| { +// if let Event::UpdatedEntries(changes) = event { +// for (path, _, change_type) in changes.iter() { +// let entry = tree.entry_for_path(&path).cloned(); +// let ix = match entries.binary_search_by_key(&path, |e| &e.path) { +// Ok(ix) | Err(ix) => ix, +// }; +// match change_type { +// PathChange::Added => entries.insert(ix, entry.unwrap()), +// PathChange::Removed => drop(entries.remove(ix)), +// PathChange::Updated => { +// let entry = entry.unwrap(); +// let existing_entry = entries.get_mut(ix).unwrap(); +// assert_eq!(existing_entry.path, entry.path); +// *existing_entry = entry; +// } +// PathChange::AddedOrUpdated | PathChange::Loaded => { +// let entry = entry.unwrap(); +// if entries.get(ix).map(|e| &e.path) == Some(&entry.path) { +// *entries.get_mut(ix).unwrap() = entry; +// } else { +// entries.insert(ix, entry); +// } +// } +// } +// } + +// let new_entries = tree.entries(true).cloned().collect::>(); +// assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes); +// } +// }) +// .detach(); +// } + +// fn randomly_mutate_worktree( +// worktree: &mut Worktree, +// rng: &mut impl Rng, +// cx: &mut ModelContext, +// ) -> Task> { +// log::info!("mutating worktree"); +// let worktree = worktree.as_local_mut().unwrap(); +// let snapshot = worktree.snapshot(); +// let entry = snapshot.entries(false).choose(rng).unwrap(); + +// match rng.gen_range(0_u32..100) { +// 0..=33 if entry.path.as_ref() != Path::new("") => { +// log::info!("deleting entry {:?} ({})", entry.path, entry.id.0); +// worktree.delete_entry(entry.id, cx).unwrap() +// } +// ..=66 if entry.path.as_ref() != Path::new("") => { +// let other_entry = snapshot.entries(false).choose(rng).unwrap(); +// let new_parent_path = if other_entry.is_dir() { +// other_entry.path.clone() +// } else { +// other_entry.path.parent().unwrap().into() +// }; +// let mut new_path = new_parent_path.join(random_filename(rng)); +// if new_path.starts_with(&entry.path) { +// new_path = random_filename(rng).into(); +// } + +// log::info!( +// "renaming entry {:?} ({}) to {:?}", +// entry.path, +// entry.id.0, +// new_path +// ); +// let task = worktree.rename_entry(entry.id, new_path, cx).unwrap(); +// cx.foreground().spawn(async move { +// task.await?; +// Ok(()) +// }) +// } +// _ => { +// let task = if entry.is_dir() { +// let child_path = entry.path.join(random_filename(rng)); +// let is_dir = rng.gen_bool(0.3); +// log::info!( +// "creating {} at {:?}", +// if is_dir { "dir" } else { "file" }, +// child_path, +// ); +// worktree.create_entry(child_path, is_dir, cx) +// } else { +// log::info!("overwriting file {:?} ({})", entry.path, entry.id.0); +// worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx) +// }; +// cx.foreground().spawn(async move { +// task.await?; +// Ok(()) +// }) +// } +// } +// } + +// async fn randomly_mutate_fs( +// fs: &Arc, +// root_path: &Path, +// insertion_probability: f64, +// rng: &mut impl Rng, +// ) { +// log::info!("mutating fs"); +// let mut files = Vec::new(); +// let mut dirs = Vec::new(); +// for path in fs.as_fake().paths(false) { +// if path.starts_with(root_path) { +// if fs.is_file(&path).await { +// files.push(path); +// } else { +// dirs.push(path); +// } +// } +// } + +// if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) { +// let path = dirs.choose(rng).unwrap(); +// let new_path = path.join(random_filename(rng)); + +// if rng.gen() { +// log::info!( +// "creating dir {:?}", +// new_path.strip_prefix(root_path).unwrap() +// ); +// fs.create_dir(&new_path).await.unwrap(); +// } else { +// log::info!( +// "creating file {:?}", +// new_path.strip_prefix(root_path).unwrap() +// ); +// fs.create_file(&new_path, Default::default()).await.unwrap(); +// } +// } else if rng.gen_bool(0.05) { +// let ignore_dir_path = dirs.choose(rng).unwrap(); +// let ignore_path = ignore_dir_path.join(&*GITIGNORE); + +// let subdirs = dirs +// .iter() +// .filter(|d| d.starts_with(&ignore_dir_path)) +// .cloned() +// .collect::>(); +// let subfiles = files +// .iter() +// .filter(|d| d.starts_with(&ignore_dir_path)) +// .cloned() +// .collect::>(); +// let files_to_ignore = { +// let len = rng.gen_range(0..=subfiles.len()); +// subfiles.choose_multiple(rng, len) +// }; +// let dirs_to_ignore = { +// let len = rng.gen_range(0..subdirs.len()); +// subdirs.choose_multiple(rng, len) +// }; + +// let mut ignore_contents = String::new(); +// for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) { +// writeln!( +// ignore_contents, +// "{}", +// path_to_ignore +// .strip_prefix(&ignore_dir_path) +// .unwrap() +// .to_str() +// .unwrap() +// ) +// .unwrap(); +// } +// log::info!( +// "creating gitignore {:?} with contents:\n{}", +// ignore_path.strip_prefix(&root_path).unwrap(), +// ignore_contents +// ); +// fs.save( +// &ignore_path, +// &ignore_contents.as_str().into(), +// Default::default(), +// ) +// .await +// .unwrap(); +// } else { +// let old_path = { +// let file_path = files.choose(rng); +// let dir_path = dirs[1..].choose(rng); +// file_path.into_iter().chain(dir_path).choose(rng).unwrap() +// }; + +// let is_rename = rng.gen(); +// if is_rename { +// let new_path_parent = dirs +// .iter() +// .filter(|d| !d.starts_with(old_path)) +// .choose(rng) +// .unwrap(); + +// let overwrite_existing_dir = +// !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3); +// let new_path = if overwrite_existing_dir { +// fs.remove_dir( +// &new_path_parent, +// RemoveOptions { +// recursive: true, +// ignore_if_not_exists: true, +// }, +// ) +// .await +// .unwrap(); +// new_path_parent.to_path_buf() +// } else { +// new_path_parent.join(random_filename(rng)) +// }; + +// log::info!( +// "renaming {:?} to {}{:?}", +// old_path.strip_prefix(&root_path).unwrap(), +// if overwrite_existing_dir { +// "overwrite " +// } else { +// "" +// }, +// new_path.strip_prefix(&root_path).unwrap() +// ); +// fs.rename( +// &old_path, +// &new_path, +// fs::RenameOptions { +// overwrite: true, +// ignore_if_exists: true, +// }, +// ) +// .await +// .unwrap(); +// } else if fs.is_file(&old_path).await { +// log::info!( +// "deleting file {:?}", +// old_path.strip_prefix(&root_path).unwrap() +// ); +// fs.remove_file(old_path, Default::default()).await.unwrap(); +// } else { +// log::info!( +// "deleting dir {:?}", +// old_path.strip_prefix(&root_path).unwrap() +// ); +// fs.remove_dir( +// &old_path, +// RemoveOptions { +// recursive: true, +// ignore_if_not_exists: true, +// }, +// ) +// .await +// .unwrap(); +// } +// } +// } + +// fn random_filename(rng: &mut impl Rng) -> String { +// (0..6) +// .map(|_| rng.sample(rand::distributions::Alphanumeric)) +// .map(char::from) +// .collect() +// } + +// #[gpui::test] +// async fn test_rename_work_directory(cx: &mut TestAppContext) { +// let root = temp_tree(json!({ +// "projects": { +// "project1": { +// "a": "", +// "b": "", +// } +// }, + +// })); +// let root_path = root.path(); + +// let tree = Worktree::local( +// build_client(cx), +// root_path, +// true, +// Arc::new(RealFs), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// let repo = git_init(&root_path.join("projects/project1")); +// git_add("a", &repo); +// git_commit("init", &repo); +// std::fs::write(root_path.join("projects/project1/a"), "aa").ok(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// tree.flush_fs_events(cx).await; + +// cx.read(|cx| { +// let tree = tree.read(cx); +// let (work_dir, _) = tree.repositories().next().unwrap(); +// assert_eq!(work_dir.as_ref(), Path::new("projects/project1")); +// assert_eq!( +// tree.status_for_file(Path::new("projects/project1/a")), +// Some(GitFileStatus::Modified) +// ); +// assert_eq!( +// tree.status_for_file(Path::new("projects/project1/b")), +// Some(GitFileStatus::Added) +// ); +// }); + +// std::fs::rename( +// root_path.join("projects/project1"), +// root_path.join("projects/project2"), +// ) +// .ok(); +// tree.flush_fs_events(cx).await; + +// cx.read(|cx| { +// let tree = tree.read(cx); +// let (work_dir, _) = tree.repositories().next().unwrap(); +// assert_eq!(work_dir.as_ref(), Path::new("projects/project2")); +// assert_eq!( +// tree.status_for_file(Path::new("projects/project2/a")), +// Some(GitFileStatus::Modified) +// ); +// assert_eq!( +// tree.status_for_file(Path::new("projects/project2/b")), +// Some(GitFileStatus::Added) +// ); +// }); +// } + +// #[gpui::test] +// async fn test_git_repository_for_path(cx: &mut TestAppContext) { +// let root = temp_tree(json!({ +// "c.txt": "", +// "dir1": { +// ".git": {}, +// "deps": { +// "dep1": { +// ".git": {}, +// "src": { +// "a.txt": "" +// } +// } +// }, +// "src": { +// "b.txt": "" +// } +// }, +// })); + +// let tree = Worktree::local( +// build_client(cx), +// root.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, _cx| { +// let tree = tree.as_local().unwrap(); + +// assert!(tree.repository_for_path("c.txt".as_ref()).is_none()); + +// let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap(); +// assert_eq!( +// entry +// .work_directory(tree) +// .map(|directory| directory.as_ref().to_owned()), +// Some(Path::new("dir1").to_owned()) +// ); + +// let entry = tree +// .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()) +// .unwrap(); +// assert_eq!( +// entry +// .work_directory(tree) +// .map(|directory| directory.as_ref().to_owned()), +// Some(Path::new("dir1/deps/dep1").to_owned()) +// ); + +// let entries = tree.files(false, 0); + +// let paths_with_repos = tree +// .entries_with_repositories(entries) +// .map(|(entry, repo)| { +// ( +// entry.path.as_ref(), +// repo.and_then(|repo| { +// repo.work_directory(&tree) +// .map(|work_directory| work_directory.0.to_path_buf()) +// }), +// ) +// }) +// .collect::>(); + +// assert_eq!( +// paths_with_repos, +// &[ +// (Path::new("c.txt"), None), +// ( +// Path::new("dir1/deps/dep1/src/a.txt"), +// Some(Path::new("dir1/deps/dep1").into()) +// ), +// (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())), +// ] +// ); +// }); + +// let repo_update_events = Arc::new(Mutex::new(vec![])); +// tree.update(cx, |_, cx| { +// let repo_update_events = repo_update_events.clone(); +// cx.subscribe(&tree, move |_, _, event, _| { +// if let Event::UpdatedGitRepositories(update) = event { +// repo_update_events.lock().push(update.clone()); +// } +// }) +// .detach(); +// }); + +// std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap(); +// tree.flush_fs_events(cx).await; + +// assert_eq!( +// repo_update_events.lock()[0] +// .iter() +// .map(|e| e.0.clone()) +// .collect::>>(), +// vec![Path::new("dir1").into()] +// ); + +// std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap(); +// tree.flush_fs_events(cx).await; + +// tree.read_with(cx, |tree, _cx| { +// let tree = tree.as_local().unwrap(); + +// assert!(tree +// .repository_for_path("dir1/src/b.txt".as_ref()) +// .is_none()); +// }); +// } + +// #[gpui::test] +// async fn test_git_status(deterministic: Arc, cx: &mut TestAppContext) { +// const IGNORE_RULE: &'static str = "**/target"; + +// let root = temp_tree(json!({ +// "project": { +// "a.txt": "a", +// "b.txt": "bb", +// "c": { +// "d": { +// "e.txt": "eee" +// } +// }, +// "f.txt": "ffff", +// "target": { +// "build_file": "???" +// }, +// ".gitignore": IGNORE_RULE +// }, + +// })); + +// const A_TXT: &'static str = "a.txt"; +// const B_TXT: &'static str = "b.txt"; +// const E_TXT: &'static str = "c/d/e.txt"; +// const F_TXT: &'static str = "f.txt"; +// const DOTGITIGNORE: &'static str = ".gitignore"; +// const BUILD_FILE: &'static str = "target/build_file"; +// let project_path = Path::new("project"); + +// // Set up git repository before creating the worktree. +// let work_dir = root.path().join("project"); +// let mut repo = git_init(work_dir.as_path()); +// repo.add_ignore_rule(IGNORE_RULE).unwrap(); +// git_add(A_TXT, &repo); +// git_add(E_TXT, &repo); +// git_add(DOTGITIGNORE, &repo); +// git_commit("Initial commit", &repo); + +// let tree = Worktree::local( +// build_client(cx), +// root.path(), +// true, +// Arc::new(RealFs), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// tree.flush_fs_events(cx).await; +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; +// deterministic.run_until_parked(); + +// // Check that the right git state is observed on startup +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); +// assert_eq!(snapshot.repositories().count(), 1); +// let (dir, _) = snapshot.repositories().next().unwrap(); +// assert_eq!(dir.as_ref(), Path::new("project")); + +// assert_eq!( +// snapshot.status_for_file(project_path.join(B_TXT)), +// Some(GitFileStatus::Added) +// ); +// assert_eq!( +// snapshot.status_for_file(project_path.join(F_TXT)), +// Some(GitFileStatus::Added) +// ); +// }); + +// // Modify a file in the working copy. +// std::fs::write(work_dir.join(A_TXT), "aa").unwrap(); +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// // The worktree detects that the file's git status has changed. +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); +// assert_eq!( +// snapshot.status_for_file(project_path.join(A_TXT)), +// Some(GitFileStatus::Modified) +// ); +// }); + +// // Create a commit in the git repository. +// git_add(A_TXT, &repo); +// git_add(B_TXT, &repo); +// git_commit("Committing modified and added", &repo); +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// // The worktree detects that the files' git status have changed. +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); +// assert_eq!( +// snapshot.status_for_file(project_path.join(F_TXT)), +// Some(GitFileStatus::Added) +// ); +// assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None); +// assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); +// }); + +// // Modify files in the working copy and perform git operations on other files. +// git_reset(0, &repo); +// git_remove_index(Path::new(B_TXT), &repo); +// git_stash(&mut repo); +// std::fs::write(work_dir.join(E_TXT), "eeee").unwrap(); +// std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap(); +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// // Check that more complex repo changes are tracked +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); + +// assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None); +// assert_eq!( +// snapshot.status_for_file(project_path.join(B_TXT)), +// Some(GitFileStatus::Added) +// ); +// assert_eq!( +// snapshot.status_for_file(project_path.join(E_TXT)), +// Some(GitFileStatus::Modified) +// ); +// }); + +// std::fs::remove_file(work_dir.join(B_TXT)).unwrap(); +// std::fs::remove_dir_all(work_dir.join("c")).unwrap(); +// std::fs::write( +// work_dir.join(DOTGITIGNORE), +// [IGNORE_RULE, "f.txt"].join("\n"), +// ) +// .unwrap(); + +// git_add(Path::new(DOTGITIGNORE), &repo); +// git_commit("Committing modified git ignore", &repo); + +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// let mut renamed_dir_name = "first_directory/second_directory"; +// const RENAMED_FILE: &'static str = "rf.txt"; + +// std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap(); +// std::fs::write( +// work_dir.join(renamed_dir_name).join(RENAMED_FILE), +// "new-contents", +// ) +// .unwrap(); + +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); +// assert_eq!( +// snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)), +// Some(GitFileStatus::Added) +// ); +// }); + +// renamed_dir_name = "new_first_directory/second_directory"; + +// std::fs::rename( +// work_dir.join("first_directory"), +// work_dir.join("new_first_directory"), +// ) +// .unwrap(); + +// tree.flush_fs_events(cx).await; +// deterministic.run_until_parked(); + +// tree.read_with(cx, |tree, _cx| { +// let snapshot = tree.snapshot(); + +// assert_eq!( +// snapshot.status_for_file( +// project_path +// .join(Path::new(renamed_dir_name)) +// .join(RENAMED_FILE) +// ), +// Some(GitFileStatus::Added) +// ); +// }); +// } + +// #[gpui::test] +// async fn test_propagate_git_statuses(cx: &mut TestAppContext) { +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root", +// json!({ +// ".git": {}, +// "a": { +// "b": { +// "c1.txt": "", +// "c2.txt": "", +// }, +// "d": { +// "e1.txt": "", +// "e2.txt": "", +// "e3.txt": "", +// } +// }, +// "f": { +// "no-status.txt": "" +// }, +// "g": { +// "h1.txt": "", +// "h2.txt": "" +// }, + +// }), +// ) +// .await; + +// fs.set_status_for_repo_via_git_operation( +// &Path::new("/root/.git"), +// &[ +// (Path::new("a/b/c1.txt"), GitFileStatus::Added), +// (Path::new("a/d/e2.txt"), GitFileStatus::Modified), +// (Path::new("g/h2.txt"), GitFileStatus::Conflict), +// ], +// ); + +// let tree = Worktree::local( +// build_client(cx), +// Path::new("/root"), +// true, +// fs.clone(), +// Default::default(), +// &mut cx.to_async(), +// ) +// .await +// .unwrap(); + +// cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) +// .await; + +// cx.foreground().run_until_parked(); +// let snapshot = tree.read_with(cx, |tree, _| tree.snapshot()); + +// check_propagated_statuses( +// &snapshot, +// &[ +// (Path::new(""), Some(GitFileStatus::Conflict)), +// (Path::new("a"), Some(GitFileStatus::Modified)), +// (Path::new("a/b"), Some(GitFileStatus::Added)), +// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), +// (Path::new("a/b/c2.txt"), None), +// (Path::new("a/d"), Some(GitFileStatus::Modified)), +// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), +// (Path::new("f"), None), +// (Path::new("f/no-status.txt"), None), +// (Path::new("g"), Some(GitFileStatus::Conflict)), +// (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)), +// ], +// ); + +// check_propagated_statuses( +// &snapshot, +// &[ +// (Path::new("a/b"), Some(GitFileStatus::Added)), +// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), +// (Path::new("a/b/c2.txt"), None), +// (Path::new("a/d"), Some(GitFileStatus::Modified)), +// (Path::new("a/d/e1.txt"), None), +// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), +// (Path::new("f"), None), +// (Path::new("f/no-status.txt"), None), +// (Path::new("g"), Some(GitFileStatus::Conflict)), +// ], +// ); + +// check_propagated_statuses( +// &snapshot, +// &[ +// (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)), +// (Path::new("a/b/c2.txt"), None), +// (Path::new("a/d/e1.txt"), None), +// (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)), +// (Path::new("f/no-status.txt"), None), +// ], +// ); + +// #[track_caller] +// fn check_propagated_statuses( +// snapshot: &Snapshot, +// expected_statuses: &[(&Path, Option)], +// ) { +// let mut entries = expected_statuses +// .iter() +// .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone()) +// .collect::>(); +// snapshot.propagate_git_statuses(&mut entries); +// assert_eq!( +// entries +// .iter() +// .map(|e| (e.path.as_ref(), e.git_status)) +// .collect::>(), +// expected_statuses +// ); +// } +// } + +// fn build_client(cx: &mut TestAppContext) -> Arc { +// let http_client = FakeHttpClient::with_404_response(); +// cx.read(|cx| Client::new(http_client, cx)) +// } + +// #[track_caller] +// fn git_init(path: &Path) -> git2::Repository { +// git2::Repository::init(path).expect("Failed to initialize git repository") +// } + +// #[track_caller] +// fn git_add>(path: P, repo: &git2::Repository) { +// let path = path.as_ref(); +// let mut index = repo.index().expect("Failed to get index"); +// index.add_path(path).expect("Failed to add a.txt"); +// index.write().expect("Failed to write index"); +// } + +// #[track_caller] +// fn git_remove_index(path: &Path, repo: &git2::Repository) { +// let mut index = repo.index().expect("Failed to get index"); +// index.remove_path(path).expect("Failed to add a.txt"); +// index.write().expect("Failed to write index"); +// } + +// #[track_caller] +// fn git_commit(msg: &'static str, repo: &git2::Repository) { +// use git2::Signature; + +// let signature = Signature::now("test", "test@zed.dev").unwrap(); +// let oid = repo.index().unwrap().write_tree().unwrap(); +// let tree = repo.find_tree(oid).unwrap(); +// if let Some(head) = repo.head().ok() { +// let parent_obj = head.peel(git2::ObjectType::Commit).unwrap(); + +// let parent_commit = parent_obj.as_commit().unwrap(); + +// repo.commit( +// Some("HEAD"), +// &signature, +// &signature, +// msg, +// &tree, +// &[parent_commit], +// ) +// .expect("Failed to commit with parent"); +// } else { +// repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[]) +// .expect("Failed to commit"); +// } +// } + +// #[track_caller] +// fn git_stash(repo: &mut git2::Repository) { +// use git2::Signature; + +// let signature = Signature::now("test", "test@zed.dev").unwrap(); +// repo.stash_save(&signature, "N/A", None) +// .expect("Failed to stash"); +// } + +// #[track_caller] +// fn git_reset(offset: usize, repo: &git2::Repository) { +// let head = repo.head().expect("Couldn't get repo head"); +// let object = head.peel(git2::ObjectType::Commit).unwrap(); +// let commit = object.as_commit().unwrap(); +// let new_head = commit +// .parents() +// .inspect(|parnet| { +// parnet.message(); +// }) +// .skip(offset) +// .next() +// .expect("Not enough history"); +// repo.reset(&new_head.as_object(), git2::ResetType::Soft, None) +// .expect("Could not reset"); +// } + +// #[allow(dead_code)] +// #[track_caller] +// fn git_status(repo: &git2::Repository) -> collections::HashMap { +// repo.statuses(None) +// .unwrap() +// .iter() +// .map(|status| (status.path().unwrap().to_string(), status.status())) +// .collect() +// }