diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 054074fb39434cefab18d6429622abfcbfea3dba..4abea30800cfb6c5d0abb88f41c5aa5139791032 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -9,15 +9,22 @@ use editor::{ Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, }; use gpui::{ - action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + action, elements::*, keymap::Binding, AnyViewHandle, AppContext, Entity, ModelHandle, + MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; -use project::{Project, ProjectPath, WorktreeId}; -use std::{cmp::Ordering, mem, ops::Range, rc::Rc, sync::Arc}; +use project::{Project, ProjectPath}; +use std::{ + any::{Any, TypeId}, + cmp::Ordering, + mem, + ops::Range, + path::PathBuf, + sync::Arc, +}; use util::TryFutureExt; -use workspace::{Navigation, Workspace}; +use workspace::{ItemNavHistory, Workspace}; action!(Deploy); action!(OpenExcerpts); @@ -49,7 +56,7 @@ struct ProjectDiagnosticsEditor { editor: ViewHandle, excerpts: ModelHandle, path_states: Vec, - paths_to_update: HashMap>, + paths_to_update: BTreeSet, build_settings: BuildSettings, settings: watch::Receiver, } @@ -119,16 +126,12 @@ impl ProjectDiagnosticsEditor { ) -> Self { let project = model.read(cx).project.clone(); cx.subscribe(&project, |this, _, event, cx| match event { - project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => { - if let Some(paths) = this.paths_to_update.remove(&worktree_id) { - this.update_excerpts(paths, cx); - } + project::Event::DiskBasedDiagnosticsFinished => { + let paths = mem::take(&mut this.paths_to_update); + this.update_excerpts(paths, cx); } project::Event::DiagnosticsUpdated(path) => { - this.paths_to_update - .entry(path.worktree_id) - .or_default() - .insert(path.clone()); + this.paths_to_update.insert(path.clone()); } _ => {} }) @@ -198,7 +201,6 @@ impl ProjectDiagnosticsEditor { } let editor = workspace .open_item(buffer, cx) - .to_any() .downcast::() .unwrap(); editor.update(cx, |editor, cx| { @@ -522,10 +524,19 @@ impl workspace::Item for ProjectDiagnostics { fn build_view( handle: ModelHandle, workspace: &Workspace, - _: Rc, + nav_history: ItemNavHistory, cx: &mut ViewContext, ) -> Self::View { - ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx) + let diagnostics = ProjectDiagnosticsEditor::new( + handle, + workspace.weak_handle(), + workspace.settings(), + cx, + ); + diagnostics + .editor + .update(cx, |editor, _| editor.set_nav_history(Some(nav_history))); + diagnostics } fn project_path(&self) -> Option { @@ -548,6 +559,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } + fn is_dirty(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).read(cx).is_dirty() } @@ -560,7 +576,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { true } - fn save(&mut self, cx: &mut ViewContext) -> Result>> { + fn save(&mut self, cx: &mut ViewContext) -> Task> { self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx)) } @@ -570,8 +586,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { fn save_as( &mut self, - _: ModelHandle, - _: &std::path::Path, + _: ModelHandle, + _: PathBuf, _: &mut ViewContext, ) -> Task> { unreachable!() @@ -592,12 +608,40 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { where Self: Sized, { - Some(ProjectDiagnosticsEditor::new( + let diagnostics = ProjectDiagnosticsEditor::new( self.model.clone(), self.workspace.clone(), self.settings.clone(), cx, - )) + ); + diagnostics.editor.update(cx, |editor, cx| { + let nav_history = self + .editor + .read(cx) + .nav_history() + .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); + editor.set_nav_history(nav_history); + }); + Some(diagnostics) + } + + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.editor).into()) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } } @@ -680,7 +724,6 @@ mod tests { use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot}; use gpui::TestAppContext; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16}; - use project::worktree; use serde_json::json; use std::sync::Arc; use unindent::Unindent as _; @@ -721,16 +764,19 @@ mod tests { ) .await; - let worktree = project + let (worktree, _) = project .update(&mut cx, |project, cx| { - project.add_local_worktree("/test", cx) + project.find_or_create_worktree_for_abs_path("/test", false, cx) }) .await .unwrap(); + let worktree_id = worktree.read_with(&cx, |tree, _| tree.id()); // Create some diagnostics worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/main.rs".as_ref()), None, @@ -882,6 +928,8 @@ mod tests { // Diagnostics are added for another earlier path. worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/consts.rs".as_ref()), None, @@ -899,7 +947,13 @@ mod tests { cx, ) .unwrap(); - cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated); + }); + project.update(&mut cx, |_, cx| { + cx.emit(project::Event::DiagnosticsUpdated(ProjectPath { + worktree_id, + path: Arc::from("/test/consts.rs".as_ref()), + })); + cx.emit(project::Event::DiskBasedDiagnosticsFinished); }); view.next_notification(&cx).await; @@ -980,6 +1034,8 @@ mod tests { // Diagnostics are added to the first path worktree.update(&mut cx, |worktree, cx| { worktree + .as_local_mut() + .unwrap() .update_diagnostic_entries( Arc::from("/test/consts.rs".as_ref()), None, @@ -1011,7 +1067,13 @@ mod tests { cx, ) .unwrap(); - cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated); + }); + project.update(&mut cx, |_, cx| { + cx.emit(project::Event::DiagnosticsUpdated(ProjectPath { + worktree_id, + path: Arc::from("/test/consts.rs".as_ref()), + })); + cx.emit(project::Event::DiskBasedDiagnosticsFinished); }); view.next_notification(&cx).await; diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 072738fa77417542fbc7cdfa1036f8d55418c6e8..3390f74a849c41cbc1268e4c05e2da6d99a18113 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -19,7 +19,7 @@ impl DiagnosticSummary { cx: &mut ViewContext, ) -> Self { cx.subscribe(project, |this, project, event, cx| match event { - project::Event::DiskBasedDiagnosticsUpdated { .. } => { + project::Event::DiskBasedDiagnosticsUpdated => { this.summary = project.read(cx).diagnostic_summary(cx); cx.notify(); } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index faf770cb1bde70fea6ab314f542395223a3c84c6..8757e7593b58137d57c9cb341149e1e069576726 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -840,7 +840,7 @@ mod tests { ("mod.body".to_string(), Color::red().into()), ("fn.name".to_string(), Color::blue().into()), ]); - let lang = Arc::new( + let language = Arc::new( Language::new( LanguageConfig { name: "Test".to_string(), @@ -857,10 +857,9 @@ mod tests { ) .unwrap(), ); - lang.set_theme(&theme); + language.set_theme(&theme); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -928,7 +927,7 @@ mod tests { ("mod.body".to_string(), Color::red().into()), ("fn.name".to_string(), Color::blue().into()), ]); - let lang = Arc::new( + let language = Arc::new( Language::new( LanguageConfig { name: "Test".to_string(), @@ -945,10 +944,9 @@ mod tests { ) .unwrap(), ); - lang.set_theme(&theme); + language.set_theme(&theme); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 96e9973df502941b6bc272d983cdbf27f5af722a..0c393d93ab68420585ec9da62b81f00f59031271 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,8 +25,8 @@ use gpui::{ use items::BufferItemHandle; use itertools::Itertools as _; use language::{ - BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, - TransactionId, + AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, + Selection, SelectionGoal, TransactionId, }; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint, @@ -41,7 +41,6 @@ use std::{ iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, - rc::Rc, sync::Arc, time::{Duration, Instant}, }; @@ -49,7 +48,7 @@ use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; use util::post_inc; -use workspace::{Navigation, PathOpener, Workspace}; +use workspace::{ItemNavHistory, PathOpener, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -107,6 +106,7 @@ action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); action!(ShowNextDiagnostic); +action!(GoToDefinition); action!(PageUp); action!(PageDown); action!(Fold); @@ -214,6 +214,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>, highlighted_rows: Option>, - navigation: Option>, + nav_history: Option, } pub struct EditorSnapshot { @@ -465,7 +467,10 @@ impl Editor { let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.navigation = self.navigation.clone(); + clone.nav_history = self + .nav_history + .as_ref() + .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); clone } @@ -515,7 +520,7 @@ impl Editor { mode: EditorMode::Full, placeholder_text: None, highlighted_rows: None, - navigation: None, + nav_history: None, }; let selection = Selection { id: post_inc(&mut this.next_selection_id), @@ -533,9 +538,8 @@ impl Editor { _: &workspace::OpenNew, cx: &mut ViewContext, ) { - let buffer = cx.add_model(|cx| { - Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx) - }); + let buffer = cx + .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); workspace.open_item(BufferItemHandle(buffer), cx); } @@ -860,7 +864,7 @@ impl Editor { } } - self.push_to_navigation_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); + self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); let selection = Selection { id: post_inc(&mut self.next_selection_id), @@ -2455,13 +2459,21 @@ impl Editor { self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } - fn push_to_navigation_history( + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + fn push_to_nav_history( &self, position: Anchor, new_position: Option, cx: &mut ViewContext, ) { - if let Some(navigation) = &self.navigation { + if let Some(nav_history) = &self.nav_history { let buffer = self.buffer.read(cx).read(cx); let offset = position.to_offset(&buffer); let point = position.to_point(&buffer); @@ -2474,13 +2486,10 @@ impl Editor { } } - navigation.push( - Some(NavigationData { - anchor: position, - offset, - }), - cx, - ); + nav_history.push(Some(NavigationData { + anchor: position, + offset, + })); } } @@ -2985,6 +2994,61 @@ impl Editor { } } + pub fn go_to_definition( + workspace: &mut Workspace, + _: &GoToDefinition, + cx: &mut ViewContext, + ) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.newest_selection::(&buffer.read(cx)).head(); + let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx); + let definitions = workspace + .project() + .update(cx, |project, cx| project.definition(&buffer, head, cx)); + cx.spawn(|workspace, mut cx| async move { + let definitions = definitions.await?; + workspace.update(&mut cx, |workspace, cx| { + for definition in definitions { + let range = definition + .target_range + .to_offset(definition.target_buffer.read(cx)); + let target_editor_handle = workspace + .open_item(BufferItemHandle(definition.target_buffer), cx) + .downcast::() + .unwrap(); + + target_editor_handle.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + let disabled_history = if editor_handle == target_editor_handle { + None + } else { + target_editor.nav_history.take() + }; + target_editor.select_ranges([range], Some(Autoscroll::Center), cx); + if disabled_history.is_some() { + target_editor.nav_history = disabled_history; + } + }); + } + }); + + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); @@ -3330,7 +3394,7 @@ impl Editor { .max_by_key(|s| s.id) .map(|s| s.head().to_point(&buffer)); if new_cursor_position.is_some() { - self.push_to_navigation_history(old_cursor_position, new_cursor_position, cx); + self.push_to_nav_history(old_cursor_position, new_cursor_position, cx); } } @@ -3995,7 +4059,7 @@ pub fn settings_builder( mod tests { use super::*; use language::LanguageConfig; - use std::time::Instant; + use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; use util::test::sample_text; @@ -4174,22 +4238,22 @@ mod tests { fn test_navigation_history(cx: &mut gpui::MutableAppContext) { cx.add_window(Default::default(), |cx| { use workspace::ItemView; - let navigation = Rc::new(workspace::Navigation::default()); + let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); let settings = EditorSettings::test(&cx); let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); let mut editor = build_editor(buffer.clone(), settings, cx); - editor.navigation = Some(navigation.clone()); + editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle())); // Move the cursor a small distance. // Nothing is added to the navigation history. editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); - assert!(navigation.pop_backward().is_none()); + assert!(nav_history.borrow_mut().pop_backward().is_none()); // Move the cursor a large distance. // The history can jump back to the previous position. editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); - let nav_entry = navigation.pop_backward().unwrap(); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); assert_eq!(nav_entry.item_view.id(), cx.view_id()); assert_eq!( @@ -4205,7 +4269,7 @@ mod tests { editor.selected_display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] ); - assert!(navigation.pop_backward().is_none()); + assert!(nav_history.borrow_mut().pop_backward().is_none()); // Move the cursor a large distance via the mouse. // The history can jump back to the previous position. @@ -4215,7 +4279,7 @@ mod tests { editor.selected_display_ranges(cx), &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] ); - let nav_entry = navigation.pop_backward().unwrap(); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); assert_eq!(nav_entry.item_view.id(), cx.view_id()); assert_eq!( @@ -5746,10 +5810,10 @@ mod tests { #[gpui::test] async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), - ))); + )); let text = r#" use mod1::mod2::{mod3, mod4}; @@ -5760,7 +5824,7 @@ mod tests { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -5887,7 +5951,7 @@ mod tests { #[gpui::test] async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new( + let language = Arc::new( Language::new( LanguageConfig { brackets: vec![ @@ -5915,11 +5979,11 @@ mod tests { "#, ) .unwrap(), - )); + ); let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); editor @@ -5944,7 +6008,7 @@ mod tests { #[gpui::test] async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ BracketPair { @@ -5963,7 +6027,7 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = r#" a @@ -5973,7 +6037,7 @@ mod tests { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -6055,13 +6119,13 @@ mod tests { #[gpui::test] async fn test_toggle_comment(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".to_string()), ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = " fn a() { @@ -6072,7 +6136,7 @@ mod tests { " .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); @@ -6323,7 +6387,7 @@ mod tests { #[gpui::test] async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { let settings = cx.read(EditorSettings::test); - let language = Some(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ BracketPair { @@ -6342,7 +6406,7 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); let text = concat!( "{ }\n", // Suppress rustfmt @@ -6352,7 +6416,7 @@ mod tests { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx)); view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 147622c8d48650ab6a303da8acff02725f42e9a2..dd4b1620272ec1976abe9322bda06b0176a1fcc0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,15 +6,15 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use postage::watch; -use project::{File, ProjectPath, Worktree}; -use std::fmt::Write; -use std::path::Path; +use project::{File, Project, ProjectPath}; +use std::path::PathBuf; use std::rc::Rc; +use std::{cell::RefCell, fmt::Write}; use text::{Point, Selection}; use util::TryFutureExt; use workspace::{ - ItemHandle, ItemView, ItemViewHandle, Navigation, PathOpener, Settings, StatusItemView, - WeakItemHandle, Workspace, + ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings, + StatusItemView, WeakItemHandle, Workspace, }; pub struct BufferOpener; @@ -28,11 +28,11 @@ struct WeakBufferItemHandle(WeakModelHandle); impl PathOpener for BufferOpener { fn open( &self, - worktree: &mut Worktree, + project: &mut Project, project_path: ProjectPath, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Option>>> { - let buffer = worktree.open_buffer(project_path.path, cx); + let buffer = project.open_buffer(project_path, cx); let task = cx.spawn(|_, _| async move { let buffer = buffer.await?; Ok(Box::new(BufferItemHandle(buffer)) as Box) @@ -46,7 +46,7 @@ impl ItemHandle for BufferItemHandle { &self, window_id: usize, workspace: &Workspace, - navigation: Rc, + nav_history: Rc>, cx: &mut MutableAppContext, ) -> Box { let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); @@ -57,7 +57,7 @@ impl ItemHandle for BufferItemHandle { crate::settings_builder(weak_buffer, workspace.settings()), cx, ); - editor.navigation = Some(navigation); + editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); editor })) } @@ -115,9 +115,9 @@ impl ItemView for Editor { }; drop(buffer); - let navigation = self.navigation.take(); + let nav_history = self.nav_history.take(); self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); - self.navigation = navigation; + self.nav_history = nav_history; } } @@ -150,7 +150,7 @@ impl ItemView for Editor { fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(selection) = self.newest_selection_internal() { - self.push_to_navigation_history(selection.head(), None, cx); + self.push_to_nav_history(selection.head(), None, cx); } } @@ -166,20 +166,18 @@ impl ItemView for Editor { self.project_path(cx).is_some() } - fn save(&mut self, cx: &mut ViewContext) -> Result>> { + fn save(&mut self, cx: &mut ViewContext) -> Task> { let buffer = self.buffer().clone(); - Ok(cx.spawn(|editor, mut cx| async move { + cx.spawn(|editor, mut cx| async move { buffer .update(&mut cx, |buffer, cx| buffer.format(cx).log_err()) .await; editor.update(&mut cx, |editor, cx| { editor.request_autoscroll(Autoscroll::Fit, cx) }); - buffer - .update(&mut cx, |buffer, cx| buffer.save(cx))? - .await?; + buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?; Ok(()) - })) + }) } fn can_save_as(&self, _: &AppContext) -> bool { @@ -188,8 +186,8 @@ impl ItemView for Editor { fn save_as( &mut self, - worktree: ModelHandle, - path: &Path, + project: ModelHandle, + abs_path: PathBuf, cx: &mut ViewContext, ) -> Task> { let buffer = self @@ -199,38 +197,8 @@ impl ItemView for Editor { .expect("cannot call save_as on an excerpt list") .clone(); - buffer.update(cx, |buffer, cx| { - let handle = cx.handle(); - let text = buffer.as_rope().clone(); - let version = buffer.version(); - - let save_as = worktree.update(cx, |worktree, cx| { - worktree - .as_local_mut() - .unwrap() - .save_buffer_as(handle, path, text, cx) - }); - - cx.spawn(|buffer, mut cx| async move { - save_as.await.map(|new_file| { - let (language, language_server) = worktree.update(&mut cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - let language = worktree - .language_registry() - .select_language(new_file.full_path()) - .cloned(); - let language_server = language - .as_ref() - .and_then(|language| worktree.register_language(language, cx)); - (language, language_server.clone()) - }); - - buffer.update(&mut cx, |buffer, cx| { - buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx); - buffer.set_language(language, language_server, cx); - }); - }) - }) + project.update(cx, |project, cx| { + project.save_buffer_as(buffer, abs_path, cx) }) } @@ -317,7 +285,7 @@ impl StatusItemView for CursorPosition { active_pane_item: Option<&dyn ItemViewHandle>, cx: &mut ViewContext, ) { - if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::()) { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); self.update_position(editor, cx); } else { @@ -403,7 +371,7 @@ impl StatusItemView for DiagnosticMessage { active_pane_item: Option<&dyn ItemViewHandle>, cx: &mut ViewContext, ) { - if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::()) { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { self._observe_active_editor = Some(cx.observe(&editor, Self::update)); self.update(editor, cx); } else { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 7ae7b5686731d8da487307cf8157431df9f28255..f3d906e27dd411fcf0938b923ac202c01b383735 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -789,6 +789,19 @@ impl MultiBuffer { cx.notify(); } + pub fn text_anchor_for_position<'a, T: ToOffset>( + &'a self, + position: T, + cx: &AppContext, + ) -> (ModelHandle, language::Anchor) { + let snapshot = self.read(cx); + let anchor = snapshot.anchor_before(position); + ( + self.buffers.borrow()[&anchor.buffer_id].buffer.clone(), + anchor.text_anchor, + ) + } + fn on_buffer_event( &mut self, _: ModelHandle, @@ -812,18 +825,18 @@ impl MultiBuffer { }) } - pub fn save(&mut self, cx: &mut ModelContext) -> Result>> { + pub fn save(&mut self, cx: &mut ModelContext) -> Task> { let mut save_tasks = Vec::new(); for BufferState { buffer, .. } in self.buffers.borrow().values() { - save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx))?); + save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx))); } - Ok(cx.spawn(|_, _| async move { + cx.spawn(|_, _| async move { for save in save_tasks { save.await?; } Ok(()) - })) + }) } pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 00f253bc75eb09634f33920c3f2d3f10d15befc5..99a60cf28d04af95abc747887a798b52d8d61093 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -454,9 +454,10 @@ mod tests { .await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); @@ -514,9 +515,10 @@ mod tests { .await; let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree("/dir".as_ref(), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/dir"), false, cx) }) .await .unwrap(); @@ -579,9 +581,14 @@ mod tests { .await; let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path( + Path::new("/root/the-parent-dir/the-file"), + false, + cx, + ) }) .await .unwrap(); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index c282cd1c7510d6ae3aaf277894fcfceecfecfd9d..68d7424ae4b10b80782234710bc6b3a0bb7c4206 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -80,17 +80,13 @@ impl GoToLine { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |cx, workspace| { - let editor = workspace - .active_item(cx) - .unwrap() - .to_any() - .downcast::() - .unwrap(); - let view = cx.add_view(|cx| GoToLine::new(editor, workspace.settings.clone(), cx)); - cx.subscribe(&view, Self::on_event).detach(); - view - }); + if let Some(editor) = workspace.active_item(cx).unwrap().downcast::() { + workspace.toggle_modal(cx, |cx, workspace| { + let view = cx.add_view(|cx| GoToLine::new(editor, workspace.settings.clone(), cx)); + cx.subscribe(&view, Self::on_event).detach(); + view + }); + } } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 725e0416a7de62297458113b15e80e5da6e74fc7..b35eda41fadea6110fa6986e5eb960db691e8c3d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -11,7 +11,7 @@ use anyhow::{anyhow, Result}; use keymap::MatchResult; use parking_lot::Mutex; use platform::Event; -use postage::{mpsc, sink::Sink as _, stream::Stream as _}; +use postage::{mpsc, oneshot, sink::Sink as _, stream::Stream as _}; use smol::prelude::*; use std::{ any::{type_name, Any, TypeId}, @@ -498,11 +498,11 @@ impl TestAppContext { .as_any_mut() .downcast_mut::() .unwrap(); - let callback = test_window + let mut done_tx = test_window .last_prompt .take() .expect("prompt was not called"); - (callback)(answer); + let _ = done_tx.try_send(answer); } } @@ -660,6 +660,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type ObservationCallback = Box bool>; +type ReleaseObservationCallback = Box; pub struct MutableAppContext { weak_self: Option>>, @@ -674,6 +675,7 @@ pub struct MutableAppContext { next_subscription_id: usize, subscriptions: Arc>>>, observations: Arc>>>, + release_observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, debug_elements_callbacks: HashMap crate::json::Value>>, @@ -717,6 +719,7 @@ impl MutableAppContext { next_subscription_id: 0, subscriptions: Default::default(), observations: Default::default(), + release_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), debug_elements_callbacks: HashMap::new(), foreground, @@ -928,61 +931,26 @@ impl MutableAppContext { self.foreground_platform.set_menus(menus); } - fn prompt( + fn prompt( &self, window_id: usize, level: PromptLevel, msg: &str, answers: &[&str], - done_fn: F, - ) where - F: 'static + FnOnce(usize, &mut MutableAppContext), - { - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let foreground = self.foreground.clone(); + ) -> oneshot::Receiver { let (_, window) = &self.presenters_and_platform_windows[&window_id]; - window.prompt( - level, - msg, - answers, - Box::new(move |answer| { - foreground - .spawn(async move { (done_fn)(answer, &mut *app.borrow_mut()) }) - .detach(); - }), - ); + window.prompt(level, msg, answers) } - pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) - where - F: 'static + FnOnce(Option>, &mut MutableAppContext), - { - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let foreground = self.foreground.clone(); - self.foreground_platform.prompt_for_paths( - options, - Box::new(move |paths| { - foreground - .spawn(async move { (done_fn)(paths, &mut *app.borrow_mut()) }) - .detach(); - }), - ); + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + self.foreground_platform.prompt_for_paths(options) } - pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) - where - F: 'static + FnOnce(Option, &mut MutableAppContext), - { - let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); - let foreground = self.foreground.clone(); - self.foreground_platform.prompt_for_new_path( - directory, - Box::new(move |path| { - foreground - .spawn(async move { (done_fn)(path, &mut *app.borrow_mut()) }) - .detach(); - }), - ); + pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + self.foreground_platform.prompt_for_new_path(directory) } pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription @@ -1071,6 +1039,27 @@ impl MutableAppContext { observations: Some(Arc::downgrade(&self.observations)), } } + + pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + E::Event: 'static, + H: Handle, + F: 'static + FnMut(&mut Self), + { + let id = post_inc(&mut self.next_subscription_id); + self.release_observations + .lock() + .entry(handle.id()) + .or_default() + .insert(id, Box::new(move |cx| callback(cx))); + Subscription::ReleaseObservation { + id, + entity_id: handle.id(), + observations: Some(Arc::downgrade(&self.release_observations)), + } + } + pub(crate) fn notify_model(&mut self, model_id: usize) { if self.pending_notifications.insert(model_id) { self.pending_effects @@ -1249,6 +1238,7 @@ impl MutableAppContext { self.cx.windows.remove(&window_id); self.presenters_and_platform_windows.remove(&window_id); self.remove_dropped_entities(); + self.flush_effects(); } fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) { @@ -1399,6 +1389,9 @@ impl MutableAppContext { self.observations.lock().remove(&model_id); let mut model = self.cx.models.remove(&model_id).unwrap(); model.release(self); + self.pending_effects.push_back(Effect::Release { + entity_id: model_id, + }); } for (window_id, view_id) in dropped_views { @@ -1422,6 +1415,9 @@ impl MutableAppContext { if let Some(view_id) = change_focus_to { self.focus(window_id, view_id); } + + self.pending_effects + .push_back(Effect::Release { entity_id: view_id }); } for key in dropped_element_states { @@ -1447,6 +1443,7 @@ impl MutableAppContext { Effect::ViewNotification { window_id, view_id } => { self.notify_view_observers(window_id, view_id) } + Effect::Release { entity_id } => self.notify_release_observers(entity_id), Effect::Focus { window_id, view_id } => { self.focus(window_id, view_id); } @@ -1609,6 +1606,15 @@ impl MutableAppContext { } } + fn notify_release_observers(&mut self, entity_id: usize) { + let callbacks = self.release_observations.lock().remove(&entity_id); + if let Some(callbacks) = callbacks { + for (_, mut callback) in callbacks { + callback(self); + } + } + } + fn focus(&mut self, window_id: usize, focused_id: usize) { if self .cx @@ -1865,6 +1871,9 @@ pub enum Effect { window_id: usize, view_id: usize, }, + Release { + entity_id: usize, + }, Focus { window_id: usize, view_id: usize, @@ -1891,6 +1900,10 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), + Effect::Release { entity_id } => f + .debug_struct("Effect::Release") + .field("entity_id", entity_id) + .finish(), Effect::Focus { window_id, view_id } => f .debug_struct("Effect::Focus") .field("window_id", window_id) @@ -2113,6 +2126,25 @@ impl<'a, T: Entity> ModelContext<'a, T> { }) } + pub fn observe_release( + &mut self, + handle: &ModelHandle, + mut callback: F, + ) -> Subscription + where + S: Entity, + F: 'static + FnMut(&mut T, &mut ModelContext), + { + let observer = self.weak_handle(); + self.app.observe_release(handle, move |cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, cx); + }); + } + }) + } + pub fn handle(&self) -> ModelHandle { ModelHandle::new(self.model_id, &self.app.cx.ref_counts) } @@ -2240,26 +2272,24 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.platform() } - pub fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str], done_fn: F) - where - F: 'static + FnOnce(usize, &mut MutableAppContext), - { - self.app - .prompt(self.window_id, level, msg, answers, done_fn) + pub fn prompt( + &self, + level: PromptLevel, + msg: &str, + answers: &[&str], + ) -> oneshot::Receiver { + self.app.prompt(self.window_id, level, msg, answers) } - pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) - where - F: 'static + FnOnce(Option>, &mut MutableAppContext), - { - self.app.prompt_for_paths(options, done_fn) + pub fn prompt_for_paths( + &self, + options: PathPromptOptions, + ) -> oneshot::Receiver>> { + self.app.prompt_for_paths(options) } - pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) - where - F: 'static + FnOnce(Option, &mut MutableAppContext), - { - self.app.prompt_for_new_path(directory, done_fn) + pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { + self.app.prompt_for_new_path(directory) } pub fn debug_elements(&self) -> crate::json::Value { @@ -2348,6 +2378,22 @@ impl<'a, T: View> ViewContext<'a, T> { }) } + pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription + where + E: Entity, + H: Handle, + F: 'static + FnMut(&mut T, &mut ViewContext), + { + let observer = self.weak_handle(); + self.app.observe_release(handle, move |cx| { + if let Some(observer) = observer.upgrade(cx) { + observer.update(cx, |observer, cx| { + callback(observer, cx); + }); + } + }) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.view_id, @@ -3306,6 +3352,12 @@ pub enum Subscription { entity_id: usize, observations: Option>>>>, }, + ReleaseObservation { + id: usize, + entity_id: usize, + observations: + Option>>>>, + }, } impl Subscription { @@ -3317,6 +3369,9 @@ impl Subscription { Subscription::Observation { observations, .. } => { observations.take(); } + Subscription::ReleaseObservation { observations, .. } => { + observations.take(); + } } } } @@ -3335,6 +3390,17 @@ impl Drop for Subscription { } } } + Subscription::ReleaseObservation { + id, + entity_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + if let Some(observations) = observations.lock().get_mut(entity_id) { + observations.remove(id); + } + } + } Subscription::Subscription { id, entity_id, @@ -3444,7 +3510,10 @@ mod tests { use super::*; use crate::elements::*; use smol::future::poll_once; - use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; + use std::{ + cell::Cell, + sync::atomic::{AtomicUsize, Ordering::SeqCst}, + }; #[crate::test(self)] fn test_model_handles(cx: &mut MutableAppContext) { @@ -3695,18 +3764,18 @@ mod tests { #[crate::test(self)] fn test_entity_release_hooks(cx: &mut MutableAppContext) { struct Model { - released: Arc>, + released: Rc>, } struct View { - released: Arc>, + released: Rc>, } impl Entity for Model { type Event = (); fn release(&mut self, _: &mut MutableAppContext) { - *self.released.lock() = true; + self.released.set(true); } } @@ -3714,7 +3783,7 @@ mod tests { type Event = (); fn release(&mut self, _: &mut MutableAppContext) { - *self.released.lock() = true; + self.released.set(true); } } @@ -3728,27 +3797,41 @@ mod tests { } } - let model_released = Arc::new(Mutex::new(false)); - let view_released = Arc::new(Mutex::new(false)); + let model_released = Rc::new(Cell::new(false)); + let model_release_observed = Rc::new(Cell::new(false)); + let view_released = Rc::new(Cell::new(false)); + let view_release_observed = Rc::new(Cell::new(false)); let model = cx.add_model(|_| Model { released: model_released.clone(), }); - - let (window_id, _) = cx.add_window(Default::default(), |_| View { + let (window_id, view) = cx.add_window(Default::default(), |_| View { released: view_released.clone(), }); + assert!(!model_released.get()); + assert!(!view_released.get()); - assert!(!*model_released.lock()); - assert!(!*view_released.lock()); + cx.observe_release(&model, { + let model_release_observed = model_release_observed.clone(); + move |_| model_release_observed.set(true) + }) + .detach(); + cx.observe_release(&view, { + let view_release_observed = view_release_observed.clone(); + move |_| view_release_observed.set(true) + }) + .detach(); cx.update(move |_| { drop(model); }); - assert!(*model_released.lock()); + assert!(model_released.get()); + assert!(model_release_observed.get()); - drop(cx.remove_window(window_id)); - assert!(*view_released.lock()); + drop(view); + cx.remove_window(window_id); + assert!(view_released.get()); + assert!(view_release_observed.get()); } #[crate::test(self)] diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 9d5acf90e98db1c88af9b5178aee3b6a1aad9724..c04145294c494d7e1b72cc4845fd6453bf25e3df 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -20,6 +20,7 @@ use crate::{ use anyhow::Result; use async_task::Runnable; pub use event::Event; +use postage::oneshot; use std::{ any::Any, path::{Path, PathBuf}, @@ -70,13 +71,8 @@ pub(crate) trait ForegroundPlatform { fn prompt_for_paths( &self, options: PathPromptOptions, - done_fn: Box>)>, - ); - fn prompt_for_new_path( - &self, - directory: &Path, - done_fn: Box)>, - ); + ) -> oneshot::Receiver>>; + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>; } pub trait Dispatcher: Send + Sync { @@ -89,13 +85,7 @@ pub trait Window: WindowContext { fn on_event(&mut self, callback: Box); fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); - fn prompt( - &self, - level: PromptLevel, - msg: &str, - answers: &[&str], - done_fn: Box, - ); + fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 9aec0b5c04ff88888388c23605ac6e9132138843..0b612c978cde8b84cd8377a5bcc836519fbe3ca7 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -33,6 +33,7 @@ use objc::{ runtime::{Class, Object, Sel}, sel, sel_impl, }; +use postage::oneshot; use ptr::null_mut; use std::{ cell::{Cell, RefCell}, @@ -248,15 +249,15 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { fn prompt_for_paths( &self, options: platform::PathPromptOptions, - done_fn: Box>)>, - ) { + ) -> oneshot::Receiver>> { unsafe { let panel = NSOpenPanel::openPanel(nil); panel.setCanChooseDirectories_(options.directories.to_objc()); panel.setCanChooseFiles_(options.files.to_objc()); panel.setAllowsMultipleSelection_(options.multiple.to_objc()); panel.setResolvesAliases_(false.to_objc()); - let done_fn = Cell::new(Some(done_fn)); + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |response: NSModalResponse| { let result = if response == NSModalResponse::NSModalResponseOk { let mut result = Vec::new(); @@ -275,27 +276,25 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { None }; - if let Some(done_fn) = done_fn.take() { - (done_fn)(result); + if let Some(mut done_tx) = done_tx.take() { + let _ = postage::sink::Sink::try_send(&mut done_tx, result); } }); let block = block.copy(); let _: () = msg_send![panel, beginWithCompletionHandler: block]; + done_rx } } - fn prompt_for_new_path( - &self, - directory: &Path, - done_fn: Box)>, - ) { + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { unsafe { let panel = NSSavePanel::savePanel(nil); let path = ns_string(directory.to_string_lossy().as_ref()); let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); panel.setDirectoryURL(url); - let done_fn = Cell::new(Some(done_fn)); + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |response: NSModalResponse| { let result = if response == NSModalResponse::NSModalResponseOk { let url = panel.URL(); @@ -311,12 +310,13 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { None }; - if let Some(done_fn) = done_fn.take() { - (done_fn)(result); + if let Some(mut done_tx) = done_tx.take() { + let _ = postage::sink::Sink::try_send(&mut done_tx, result); } }); let block = block.copy(); let _: () = msg_send![panel, beginWithCompletionHandler: block]; + done_rx } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 2d6ea0055e84a9ce17c532101fb528e9138ee135..ba75964039e9c36a7d439a3e2246fd3c9fca9b7c 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -28,6 +28,7 @@ use objc::{ runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, sel, sel_impl, }; +use postage::oneshot; use smol::Timer; use std::{ any::Any, @@ -317,8 +318,7 @@ impl platform::Window for Window { level: platform::PromptLevel, msg: &str, answers: &[&str], - done_fn: Box, - ) { + ) -> oneshot::Receiver { unsafe { let alert: id = msg_send![class!(NSAlert), alloc]; let alert: id = msg_send![alert, init]; @@ -333,10 +333,11 @@ impl platform::Window for Window { let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; let _: () = msg_send![button, setTag: ix as NSInteger]; } - let done_fn = Cell::new(Some(done_fn)); + let (done_tx, done_rx) = oneshot::channel(); + let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |answer: NSInteger| { - if let Some(done_fn) = done_fn.take() { - (done_fn)(answer.try_into().unwrap()); + if let Some(mut done_tx) = done_tx.take() { + let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap()); } }); let block = block.copy(); @@ -345,6 +346,7 @@ impl platform::Window for Window { beginSheetModalForWindow: self.0.borrow().native_window completionHandler: block ]; + done_rx } } diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index bffa9858159e67d1145af635d2bcca9951fe6c3e..3e3ec60acb10fc4d67b66ba5f6064b8e67c9f21f 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -5,9 +5,10 @@ use crate::{ }; use anyhow::{anyhow, Result}; use parking_lot::Mutex; +use postage::oneshot; use std::{ any::Any, - cell::RefCell, + cell::{Cell, RefCell}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -23,7 +24,7 @@ pub struct Platform { #[derive(Default)] pub struct ForegroundPlatform { - last_prompt_for_new_path_args: RefCell)>)>>, + last_prompt_for_new_path_args: RefCell>)>>, } struct Dispatcher; @@ -35,7 +36,7 @@ pub struct Window { event_handlers: Vec>, resize_handlers: Vec>, close_handlers: Vec>, - pub(crate) last_prompt: RefCell>>, + pub(crate) last_prompt: Cell>>, } impl ForegroundPlatform { @@ -43,11 +44,11 @@ impl ForegroundPlatform { &self, result: impl FnOnce(PathBuf) -> Option, ) { - let (dir_path, callback) = self + let (dir_path, mut done_tx) = self .last_prompt_for_new_path_args .take() .expect("prompt_for_new_path was not called"); - callback(result(dir_path)); + let _ = postage::sink::Sink::try_send(&mut done_tx, result(dir_path)); } pub(crate) fn did_prompt_for_new_path(&self) -> bool { @@ -77,12 +78,15 @@ impl super::ForegroundPlatform for ForegroundPlatform { fn prompt_for_paths( &self, _: super::PathPromptOptions, - _: Box>)>, - ) { + ) -> oneshot::Receiver>> { + let (_done_tx, done_rx) = oneshot::channel(); + done_rx } - fn prompt_for_new_path(&self, path: &Path, f: Box)>) { - *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f)); + fn prompt_for_new_path(&self, path: &Path) -> oneshot::Receiver> { + let (done_tx, done_rx) = oneshot::channel(); + *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx)); + done_rx } } @@ -170,7 +174,7 @@ impl Window { close_handlers: Vec::new(), scale_factor: 1.0, current_scene: None, - last_prompt: RefCell::new(None), + last_prompt: Default::default(), } } } @@ -220,8 +224,10 @@ impl super::Window for Window { self.close_handlers.push(callback); } - fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box) { - self.last_prompt.replace(Some(f)); + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver { + let (done_tx, done_rx) = oneshot::channel(); + self.last_prompt.replace(Some(done_tx)); + done_rx } fn activate(&self) {} diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 8adda8e1d37224d2a1df342e411f1ed1d314cd7a..460423f6dc7107e56563c442081c0942e6446108 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -55,7 +55,7 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut MutableAppContext) { .await; if let Some(Some(Ok(item))) = opened.first() { - if let Some(editor) = item.to_any().downcast::() { + if let Some(editor) = item.downcast::() { editor.update(&mut cx, |editor, cx| { let len = editor.buffer().read(cx).read(cx).len(); editor.select_ranges([len..len], Some(Autoscroll::Center), cx); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4cf0c8f0813eb26cd7309892274f41609fa9b9a8..042c0148449240261672d08ba24169a23bb570ac 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -385,13 +385,17 @@ impl Buffer { } } - pub fn with_language( + pub fn with_language(mut self, language: Arc, cx: &mut ModelContext) -> Self { + self.set_language(Some(language), cx); + self + } + + pub fn with_language_server( mut self, - language: Option>, - language_server: Option>, + server: Arc, cx: &mut ModelContext, ) -> Self { - self.set_language(language, language_server, cx); + self.set_language_server(Some(server), cx); self } @@ -506,30 +510,34 @@ impl Buffer { pub fn save( &mut self, cx: &mut ModelContext, - ) -> Result>> { - let file = self - .file - .as_ref() - .ok_or_else(|| anyhow!("buffer has no file"))?; + ) -> Task> { + let file = if let Some(file) = self.file.as_ref() { + file + } else { + return Task::ready(Err(anyhow!("buffer has no file"))); + }; let text = self.as_rope().clone(); let version = self.version(); let save = file.save(self.remote_id(), text, version, cx.as_mut()); - Ok(cx.spawn(|this, mut cx| async move { + cx.spawn(|this, mut cx| async move { let (version, mtime) = save.await?; this.update(&mut cx, |this, cx| { this.did_save(version.clone(), mtime, None, cx); }); Ok((version, mtime)) - })) + }) + } + + pub fn set_language(&mut self, language: Option>, cx: &mut ModelContext) { + self.language = language; + self.reparse(cx); } - pub fn set_language( + pub fn set_language_server( &mut self, - language: Option>, language_server: Option>, cx: &mut ModelContext, ) { - self.language = language; self.language_server = if let Some(server) = language_server { let (latest_snapshot_tx, mut latest_snapshot_rx) = watch::channel(); Some(LanguageServerState { @@ -611,7 +619,6 @@ impl Buffer { None }; - self.reparse(cx); self.update_language_server(); } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index e2ee035c86ac4d36a4cbeeebebe265ab6023a2a6..065ca28cec4d5aa3474e975e6290e3baac377bcf 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -145,9 +145,8 @@ async fn test_apply_diff(mut cx: gpui::TestAppContext) { #[gpui::test] async fn test_reparse(mut cx: gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| { - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx) - }); + let buffer = + cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); // Wait for the initial text to parse buffer @@ -280,7 +279,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) { #[gpui::test] async fn test_outline(mut cx: gpui::TestAppContext) { - let language = Some(Arc::new( + let language = Arc::new( rust_lang() .with_outline_query( r#" @@ -308,7 +307,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { "#, ) .unwrap(), - )); + ); let text = r#" struct Person { @@ -337,7 +336,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let outline = buffer .read_with(&cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -422,7 +421,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { } " .unindent(); - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx) + Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx) }); let buffer = buffer.read(cx); assert_eq!( @@ -452,8 +451,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { fn test_edit_with_autoindent(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent([8..8], "\n\n", cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -479,8 +477,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta " .unindent(); - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); // Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // their indentation is not adjusted. @@ -529,8 +526,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte " .unindent(); - let mut buffer = - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx); + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); buffer.edit_with_autoindent([5..5], "\nb", cx); assert_eq!( @@ -575,7 +571,9 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { .unindent(); let buffer = cx.add_model(|cx| { - Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx) + Buffer::new(0, text, cx) + .with_language(Arc::new(rust_lang), cx) + .with_language_server(language_server, cx) }); let open_notification = fake @@ -849,7 +847,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { ); let mut buffer = Buffer::new(0, text, cx); - buffer.set_language(Some(Arc::new(rust_lang())), None, cx); + buffer.set_language(Some(Arc::new(rust_lang())), cx); buffer .update_diagnostics( None, diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ad4355e90293fcd6811768514213cadb63c8fa6c..abadb6922f0e4169fe1d557b002929999571b654 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -205,8 +205,7 @@ impl LanguageServer { output_done_rx: Mutex::new(Some(output_done_rx)), }); - let root_uri = - lsp_types::Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?; + let root_uri = Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?; executor .spawn({ let this = this.clone(); @@ -220,18 +219,25 @@ impl LanguageServer { Ok(this) } - async fn init(self: Arc, root_uri: lsp_types::Url) -> Result<()> { + async fn init(self: Arc, root_uri: Url) -> Result<()> { #[allow(deprecated)] - let params = lsp_types::InitializeParams { + let params = InitializeParams { process_id: Default::default(), root_path: Default::default(), root_uri: Some(root_uri), initialization_options: Default::default(), - capabilities: lsp_types::ClientCapabilities { + capabilities: ClientCapabilities { + text_document: Some(TextDocumentClientCapabilities { + definition: Some(GotoCapability { + link_support: Some(true), + ..Default::default() + }), + ..Default::default() + }), experimental: Some(json!({ "serverStatusNotification": true, })), - window: Some(lsp_types::WindowClientCapabilities { + window: Some(WindowClientCapabilities { work_done_progress: Some(true), ..Default::default() }), @@ -244,16 +250,16 @@ impl LanguageServer { }; let this = self.clone(); - let request = Self::request_internal::( + let request = Self::request_internal::( &this.next_id, &this.response_handlers, this.outbound_tx.read().as_ref(), params, ); request.await?; - Self::notify_internal::( + Self::notify_internal::( this.outbound_tx.read().as_ref(), - lsp_types::InitializedParams {}, + InitializedParams {}, )?; Ok(()) } @@ -265,14 +271,14 @@ impl LanguageServer { let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); let mut output_done = self.output_done_rx.lock().take().unwrap(); Some(async move { - Self::request_internal::( + Self::request_internal::( &next_id, &response_handlers, outbound_tx.as_ref(), (), ) .await?; - Self::notify_internal::(outbound_tx.as_ref(), ())?; + Self::notify_internal::(outbound_tx.as_ref(), ())?; drop(outbound_tx); output_done.recv().await; drop(tasks); @@ -285,7 +291,7 @@ impl LanguageServer { pub fn on_notification(&self, mut f: F) -> Subscription where - T: lsp_types::notification::Notification, + T: notification::Notification, F: 'static + Send + Sync + FnMut(T::Params), { let prev_handler = self.notification_handlers.write().insert( @@ -309,8 +315,8 @@ impl LanguageServer { } } - pub fn request( - self: Arc, + pub fn request( + self: &Arc, params: T::Params, ) -> impl Future> where @@ -329,7 +335,7 @@ impl LanguageServer { } } - fn request_internal( + fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>, outbound_tx: Option<&channel::Sender>>, @@ -376,7 +382,7 @@ impl LanguageServer { } } - pub fn notify( + pub fn notify( self: &Arc, params: T::Params, ) -> impl Future> { @@ -388,7 +394,7 @@ impl LanguageServer { } } - fn notify_internal( + fn notify_internal( outbound_tx: Option<&channel::Sender>>, params: T::Params, ) -> Result<()> { @@ -601,8 +607,7 @@ mod tests { "lib.rs": &lib_source } })); - let lib_file_uri = - lsp_types::Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap(); + let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap(); let server = cx.read(|cx| { LanguageServer::new( @@ -615,24 +620,22 @@ mod tests { server.next_idle_notification().await; server - .notify::( - lsp_types::DidOpenTextDocumentParams { - text_document: lsp_types::TextDocumentItem::new( - lib_file_uri.clone(), - "rust".to_string(), - 0, - lib_source, - ), - }, - ) + .notify::(DidOpenTextDocumentParams { + text_document: TextDocumentItem::new( + lib_file_uri.clone(), + "rust".to_string(), + 0, + lib_source, + ), + }) .await .unwrap(); let hover = server - .request::(lsp_types::HoverParams { - text_document_position_params: lsp_types::TextDocumentPositionParams { - text_document: lsp_types::TextDocumentIdentifier::new(lib_file_uri), - position: lsp_types::Position::new(1, 21), + .request::(HoverParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier::new(lib_file_uri), + position: Position::new(1, 21), }, work_done_progress_params: Default::default(), }) @@ -641,8 +644,8 @@ mod tests { .unwrap(); assert_eq!( hover.contents, - lsp_types::HoverContents::Markup(lsp_types::MarkupContent { - kind: lsp_types::MarkupKind::Markdown, + HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, value: "&str".to_string() }) ); @@ -705,10 +708,9 @@ mod tests { ); drop(server); - let (shutdown_request, _) = fake.receive_request::().await; + let (shutdown_request, _) = fake.receive_request::().await; fake.respond(shutdown_request, ()).await; - fake.receive_notification::() - .await; + fake.receive_notification::().await; } impl LanguageServer { @@ -726,7 +728,7 @@ mod tests { pub enum ServerStatusNotification {} - impl lsp_types::notification::Notification for ServerStatusNotification { + impl notification::Notification for ServerStatusNotification { type Params = ServerStatusParams; const METHOD: &'static str = "experimental/serverStatus"; } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 46bf088c31479b0bcf26d40b70933cc3947080c3..151e3ec0bb3187caea98d0fe0b8f8464baaae80f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -144,7 +144,7 @@ impl OutlineView { fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { if let Some(editor) = workspace .active_item(cx) - .and_then(|item| item.to_any().downcast::()) + .and_then(|item| item.downcast::()) { let settings = workspace.settings(); let buffer = editor diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a8b38a2bb20cdb6a988cc809573ab4142046a6a8..f01a875011ef29e733a71815eb056816e3caa62c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5,35 +5,46 @@ pub mod worktree; use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; -use collections::HashMap; +use collections::{hash_map, HashMap, HashSet}; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, + WeakModelHandle, }; -use language::{Buffer, DiagnosticEntry, LanguageRegistry}; -use lsp::DiagnosticSeverity; +use language::{ + Bias, Buffer, DiagnosticEntry, File as _, Language, LanguageRegistry, ToOffset, ToPointUtf16, +}; +use lsp::{DiagnosticSeverity, LanguageServer}; use postage::{prelude::Stream, watch}; +use smol::block_on; use std::{ - path::Path, + ops::Range, + path::{Path, PathBuf}, sync::{atomic::AtomicBool, Arc}, }; -use util::TryFutureExt as _; +use util::{ResultExt, TryFutureExt as _}; pub use fs::*; pub use worktree::*; pub struct Project { - worktrees: Vec>, + worktrees: Vec, active_entry: Option, languages: Arc, + language_servers: HashMap<(WorktreeId, String), Arc>, client: Arc, user_store: ModelHandle, fs: Arc, client_state: ProjectClientState, collaborators: HashMap, subscriptions: Vec, - pending_disk_based_diagnostics: isize, + language_servers_with_diagnostics_running: isize, +} + +enum WorktreeHandle { + Strong(ModelHandle), + Weak(WeakModelHandle), } enum ProjectClientState { @@ -57,12 +68,12 @@ pub struct Collaborator { pub replica_id: ReplicaId, } -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), DiskBasedDiagnosticsStarted, - DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId }, + DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), } @@ -81,6 +92,12 @@ pub struct DiagnosticSummary { pub hint_count: usize, } +#[derive(Debug)] +pub struct Definition { + pub target_buffer: ModelHandle, + pub target_range: Range, +} + impl DiagnosticSummary { fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { let mut this = Self { @@ -148,16 +165,13 @@ impl Project { if let Some(project_id) = remote_id { let mut registrations = Vec::new(); - this.read_with(&cx, |this, cx| { - for worktree in &this.worktrees { - let worktree_id = worktree.id() as u64; - let worktree = worktree.read(cx).as_local().unwrap(); - registrations.push(rpc.request( - proto::RegisterWorktree { - project_id, - worktree_id, - root_name: worktree.root_name().to_string(), - authorized_logins: worktree.authorized_logins(), + this.update(&mut cx, |this, cx| { + for worktree in this.worktrees(cx).collect::>() { + registrations.push(worktree.update( + cx, + |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree.register(project_id, cx) }, )); } @@ -190,7 +204,8 @@ impl Project { client, user_store, fs, - pending_disk_based_diagnostics: 0, + language_servers_with_diagnostics_running: 0, + language_servers: Default::default(), } }) } @@ -222,7 +237,6 @@ impl Project { worktree, client.clone(), user_store.clone(), - languages.clone(), cx, ) .await?, @@ -282,10 +296,11 @@ impl Project { remote_id, replica_id, }, - pending_disk_based_diagnostics: 0, + language_servers_with_diagnostics_running: 0, + language_servers: Default::default(), }; for worktree in worktrees { - this.add_worktree(worktree, cx); + this.add_worktree(&worktree, cx); } this })) @@ -354,8 +369,13 @@ impl Project { &self.collaborators } - pub fn worktrees(&self) -> &[ModelHandle] { - &self.worktrees + pub fn worktrees<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.worktrees + .iter() + .filter_map(move |worktree| worktree.upgrade(cx)) } pub fn worktree_for_id( @@ -363,10 +383,8 @@ impl Project { id: WorktreeId, cx: &AppContext, ) -> Option> { - self.worktrees - .iter() + self.worktrees(cx) .find(|worktree| worktree.read(cx).id() == id) - .cloned() } pub fn share(&self, cx: &mut ModelContext) -> Task> { @@ -391,10 +409,10 @@ impl Project { rpc.request(proto::ShareProject { project_id }).await?; let mut tasks = Vec::new(); this.update(&mut cx, |this, cx| { - for worktree in &this.worktrees { + for worktree in this.worktrees(cx).collect::>() { worktree.update(cx, |worktree, cx| { let worktree = worktree.as_local_mut().unwrap(); - tasks.push(worktree.share(project_id, cx)); + tasks.push(worktree.share(cx)); }); } }); @@ -428,7 +446,7 @@ impl Project { rpc.send(proto::UnshareProject { project_id }).await?; this.update(&mut cx, |this, cx| { this.collaborators.clear(); - for worktree in &this.worktrees { + for worktree in this.worktrees(cx).collect::>() { worktree.update(cx, |worktree, _| { worktree.as_local_mut().unwrap().unshare(); }); @@ -457,15 +475,397 @@ impl Project { } pub fn open_buffer( - &self, + &mut self, path: ProjectPath, cx: &mut ModelContext, ) -> Task>> { - if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { - worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx)) + let worktree = if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { + worktree + } else { + return cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) }); + }; + let buffer_task = worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx)); + cx.spawn(|this, mut cx| async move { + let (buffer, buffer_is_new) = buffer_task.await?; + if buffer_is_new { + this.update(&mut cx, |this, cx| { + this.assign_language_to_buffer(worktree, buffer.clone(), cx) + }); + } + Ok(buffer) + }) + } + + pub fn save_buffer_as( + &self, + buffer: ModelHandle, + abs_path: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let worktree_task = self.find_or_create_worktree_for_abs_path(&abs_path, false, cx); + cx.spawn(|this, mut cx| async move { + let (worktree, path) = worktree_task.await?; + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .save_buffer_as(buffer.clone(), path, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.assign_language_to_buffer(worktree, buffer, cx) + }); + Ok(()) + }) + } + + fn assign_language_to_buffer( + &mut self, + worktree: ModelHandle, + buffer: ModelHandle, + cx: &mut ModelContext, + ) -> Option<()> { + // Set the buffer's language + let full_path = buffer.read(cx).file()?.full_path(); + let language = self.languages.select_language(&full_path)?.clone(); + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(language.clone()), cx); + }); + + // For local worktrees, start a language server if needed. + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let worktree_abs_path = worktree.as_local()?.abs_path().clone(); + let language_server = match self + .language_servers + .entry((worktree_id, language.name().to_string())) + { + hash_map::Entry::Occupied(e) => Some(e.get().clone()), + hash_map::Entry::Vacant(e) => { + Self::start_language_server(self.client.clone(), language, &worktree_abs_path, cx) + .map(|server| e.insert(server).clone()) + } + }; + + buffer.update(cx, |buffer, cx| { + buffer.set_language_server(language_server, cx) + }); + + None + } + + fn start_language_server( + rpc: Arc, + language: Arc, + worktree_path: &Path, + cx: &mut ModelContext, + ) -> Option> { + enum LspEvent { + DiagnosticsStart, + DiagnosticsUpdate(lsp::PublishDiagnosticsParams), + DiagnosticsFinish, + } + + let language_server = language + .start_server(worktree_path, cx) + .log_err() + .flatten()?; + let disk_based_sources = language + .disk_based_diagnostic_sources() + .cloned() + .unwrap_or_default(); + let disk_based_diagnostics_progress_token = + language.disk_based_diagnostics_progress_token().cloned(); + let has_disk_based_diagnostic_progress_token = + disk_based_diagnostics_progress_token.is_some(); + let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); + + // Listen for `PublishDiagnostics` notifications. + language_server + .on_notification::({ + let diagnostics_tx = diagnostics_tx.clone(); + move |params| { + if !has_disk_based_diagnostic_progress_token { + block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok(); + } + block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok(); + if !has_disk_based_diagnostic_progress_token { + block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok(); + } + } + }) + .detach(); + + // Listen for `Progress` notifications. Send an event when the language server + // transitions between running jobs and not running any jobs. + let mut running_jobs_for_this_server: i32 = 0; + language_server + .on_notification::(move |params| { + let token = match params.token { + lsp::NumberOrString::Number(_) => None, + lsp::NumberOrString::String(token) => Some(token), + }; + + if token == disk_based_diagnostics_progress_token { + match params.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + running_jobs_for_this_server += 1; + if running_jobs_for_this_server == 1 { + block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok(); + } + } + lsp::WorkDoneProgress::End(_) => { + running_jobs_for_this_server -= 1; + if running_jobs_for_this_server == 0 { + block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok(); + } + } + _ => {} + }, + } + } + }) + .detach(); + + // Process all the LSP events. + cx.spawn_weak(|this, mut cx| async move { + while let Ok(message) = diagnostics_rx.recv().await { + let this = cx.read(|cx| this.upgrade(cx))?; + match message { + LspEvent::DiagnosticsStart => { + let send = this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_started(cx); + this.remote_id().map(|project_id| { + rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id }) + }) + }); + if let Some(send) = send { + send.await.log_err(); + } + } + LspEvent::DiagnosticsUpdate(params) => { + this.update(&mut cx, |this, cx| { + this.update_diagnostics(params, &disk_based_sources, cx) + .log_err(); + }); + } + LspEvent::DiagnosticsFinish => { + let send = this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_finished(cx); + this.remote_id().map(|project_id| { + rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id }) + }) + }); + if let Some(send) = send { + send.await.log_err(); + } + } + } + } + Some(()) + }) + .detach(); + + Some(language_server) + } + + fn update_diagnostics( + &mut self, + diagnostics: lsp::PublishDiagnosticsParams, + disk_based_sources: &HashSet, + cx: &mut ModelContext, + ) -> Result<()> { + let path = diagnostics + .uri + .to_file_path() + .map_err(|_| anyhow!("URI is not a file"))?; + let (worktree, relative_path) = self + .find_worktree_for_abs_path(&path, cx) + .ok_or_else(|| anyhow!("no worktree found for diagnostics"))?; + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }; + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().update_diagnostics( + project_path.path.clone(), + diagnostics, + disk_based_sources, + cx, + ) + })?; + cx.emit(Event::DiagnosticsUpdated(project_path)); + Ok(()) + } + + pub fn definition( + &self, + source_buffer_handle: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + let source_buffer_handle = source_buffer_handle.clone(); + let buffer = source_buffer_handle.read(cx); + let worktree; + let buffer_abs_path; + if let Some(file) = File::from_dyn(buffer.file()) { + worktree = file.worktree.clone(); + buffer_abs_path = file.abs_path(); + } else { + return Task::ready(Err(anyhow!("buffer does not belong to any worktree"))); + }; + + if worktree.read(cx).as_local().is_some() { + let point = buffer.offset_to_point_utf16(position.to_offset(buffer)); + let buffer_abs_path = buffer_abs_path.unwrap(); + let lang_name; + let lang_server; + if let Some(lang) = buffer.language() { + lang_name = lang.name().to_string(); + if let Some(server) = self + .language_servers + .get(&(worktree.read(cx).id(), lang_name.clone())) + { + lang_server = server.clone(); + } else { + return Task::ready(Err(anyhow!("buffer does not have a language server"))); + }; + } else { + return Task::ready(Err(anyhow!("buffer does not have a language"))); + } + + cx.spawn(|this, mut cx| async move { + let response = lang_server + .request::(lsp::GotoDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier::new( + lsp::Url::from_file_path(&buffer_abs_path).unwrap(), + ), + position: lsp::Position::new(point.row, point.column), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }) + .await?; + + let mut definitions = Vec::new(); + if let Some(response) = response { + let mut unresolved_locations = Vec::new(); + match response { + lsp::GotoDefinitionResponse::Scalar(loc) => { + unresolved_locations.push((loc.uri, loc.range)); + } + lsp::GotoDefinitionResponse::Array(locs) => { + unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); + } + lsp::GotoDefinitionResponse::Link(links) => { + unresolved_locations.extend( + links + .into_iter() + .map(|l| (l.target_uri, l.target_selection_range)), + ); + } + } + + for (target_uri, target_range) in unresolved_locations { + let abs_path = target_uri + .to_file_path() + .map_err(|_| anyhow!("invalid target path"))?; + + let (worktree, relative_path) = if let Some(result) = this + .read_with(&cx, |this, cx| { + this.find_worktree_for_abs_path(&abs_path, cx) + }) { + result + } else { + let (worktree, relative_path) = this + .update(&mut cx, |this, cx| { + this.create_worktree_for_abs_path(&abs_path, true, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + this.language_servers.insert( + (worktree.read(cx).id(), lang_name.clone()), + lang_server.clone(), + ); + }); + (worktree, relative_path) + }; + + let project_path = ProjectPath { + worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), + path: relative_path.into(), + }; + let target_buffer_handle = this + .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await?; + cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer + .clip_point_utf16(target_range.start.to_point_utf16(), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(target_range.end.to_point_utf16(), Bias::Left); + definitions.push(Definition { + target_buffer: target_buffer_handle, + target_range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }); + }); + } + } + + Ok(definitions) + }) + } else { + log::info!("go to definition is not yet implemented for guests"); + Task::ready(Ok(Default::default())) + } + } + + pub fn find_or_create_worktree_for_abs_path( + &self, + abs_path: impl AsRef, + weak: bool, + cx: &mut ModelContext, + ) -> Task, PathBuf)>> { + let abs_path = abs_path.as_ref(); + if let Some((tree, relative_path)) = self.find_worktree_for_abs_path(abs_path, cx) { + Task::ready(Ok((tree.clone(), relative_path.into()))) } else { - cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) }) + self.create_worktree_for_abs_path(abs_path, weak, cx) + } + } + + fn create_worktree_for_abs_path( + &self, + abs_path: &Path, + weak: bool, + cx: &mut ModelContext, + ) -> Task, PathBuf)>> { + let worktree = self.add_local_worktree(abs_path, weak, cx); + cx.background().spawn(async move { + let worktree = worktree.await?; + Ok((worktree, PathBuf::new())) + }) + } + + fn find_worktree_for_abs_path( + &self, + abs_path: &Path, + cx: &AppContext, + ) -> Option<(ModelHandle, PathBuf)> { + for tree in self.worktrees(cx) { + 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 { @@ -475,42 +875,35 @@ impl Project { } } - pub fn add_local_worktree( - &mut self, + fn add_local_worktree( + &self, abs_path: impl AsRef, + weak: bool, cx: &mut ModelContext, ) -> Task>> { let fs = self.fs.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); - let languages = self.languages.clone(); let path = Arc::from(abs_path.as_ref()); cx.spawn(|project, mut cx| async move { let worktree = - Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx) - .await?; + Worktree::open_local(client.clone(), user_store, path, weak, fs, &mut cx).await?; let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| { - project.add_worktree(worktree.clone(), cx); + project.add_worktree(&worktree, cx); (project.remote_id(), project.is_shared()) }); if let Some(project_id) = remote_project_id { - let worktree_id = worktree.id() as u64; - let register_message = worktree.update(&mut cx, |worktree, _| { - let worktree = worktree.as_local_mut().unwrap(); - proto::RegisterWorktree { - project_id, - worktree_id, - root_name: worktree.root_name().to_string(), - authorized_logins: worktree.authorized_logins(), - } - }); - client.request(register_message).await?; + worktree + .update(&mut cx, |worktree, cx| { + worktree.as_local_mut().unwrap().register(project_id, cx) + }) + .await?; if is_shared { worktree .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().share(project_id, cx) + worktree.as_local_mut().unwrap().share(cx) }) .await?; } @@ -520,35 +913,35 @@ impl Project { }) } - fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { + pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext) { + self.worktrees.retain(|worktree| { + worktree + .upgrade(cx) + .map_or(false, |w| w.read(cx).id() != id) + }); + cx.notify(); + } + + fn add_worktree(&mut self, worktree: &ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, move |this, worktree, event, cx| match event { - worktree::Event::DiagnosticsUpdated(path) => { - cx.emit(Event::DiagnosticsUpdated(ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.clone(), - })); - } - worktree::Event::DiskBasedDiagnosticsUpdating => { - if this.pending_disk_based_diagnostics == 0 { - cx.emit(Event::DiskBasedDiagnosticsStarted); - } - this.pending_disk_based_diagnostics += 1; - } - worktree::Event::DiskBasedDiagnosticsUpdated => { - this.pending_disk_based_diagnostics -= 1; - cx.emit(Event::DiskBasedDiagnosticsUpdated { - worktree_id: worktree.read(cx).id(), - }); - if this.pending_disk_based_diagnostics == 0 { - if this.pending_disk_based_diagnostics == 0 { - cx.emit(Event::DiskBasedDiagnosticsFinished); - } - } - } - }) - .detach(); - self.worktrees.push(worktree); + + let push_weak_handle = { + let worktree = worktree.read(cx); + worktree.is_local() && worktree.is_weak() + }; + if push_weak_handle { + cx.observe_release(&worktree, |this, cx| { + this.worktrees + .retain(|worktree| worktree.upgrade(cx).is_some()); + cx.notify(); + }) + .detach(); + self.worktrees + .push(WorktreeHandle::Weak(worktree.downgrade())); + } else { + self.worktrees + .push(WorktreeHandle::Strong(worktree.clone())); + } cx.notify(); } @@ -568,7 +961,7 @@ impl Project { } pub fn is_running_disk_based_diagnostics(&self) -> bool { - self.pending_disk_based_diagnostics > 0 + self.language_servers_with_diagnostics_running > 0 } pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary { @@ -586,7 +979,7 @@ impl Project { &'a self, cx: &'a AppContext, ) -> impl Iterator + 'a { - self.worktrees.iter().flat_map(move |worktree| { + self.worktrees(cx).flat_map(move |worktree| { let worktree = worktree.read(cx); let worktree_id = worktree.id(); worktree @@ -595,6 +988,21 @@ impl Project { }) } + fn disk_based_diagnostics_started(&mut self, cx: &mut ModelContext) { + self.language_servers_with_diagnostics_running += 1; + if self.language_servers_with_diagnostics_running == 1 { + cx.emit(Event::DiskBasedDiagnosticsStarted); + } + } + + fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext) { + cx.emit(Event::DiskBasedDiagnosticsUpdated); + self.language_servers_with_diagnostics_running -= 1; + if self.language_servers_with_diagnostics_running == 0 { + cx.emit(Event::DiskBasedDiagnosticsFinished); + } + } + pub fn active_entry(&self) -> Option { self.active_entry } @@ -664,7 +1072,7 @@ impl Project { .remove(&peer_id) .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? .replica_id; - for worktree in &self.worktrees { + for worktree in self.worktrees(cx).collect::>() { worktree.update(cx, |worktree, cx| { worktree.remove_collaborator(peer_id, replica_id, cx); }) @@ -685,14 +1093,12 @@ impl Project { .worktree .ok_or_else(|| anyhow!("invalid worktree"))?; let user_store = self.user_store.clone(); - let languages = self.languages.clone(); cx.spawn(|this, mut cx| { async move { - let worktree = Worktree::remote( - remote_id, replica_id, worktree, client, user_store, languages, &mut cx, - ) - .await?; - this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx)); + let worktree = + Worktree::remote(remote_id, replica_id, worktree, client, user_store, &mut cx) + .await?; + this.update(&mut cx, |this, cx| this.add_worktree(&worktree, cx)); Ok(()) } .log_err() @@ -708,9 +1114,7 @@ impl Project { cx: &mut ModelContext, ) -> Result<()> { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - self.worktrees - .retain(|worktree| worktree.read(cx).as_remote().unwrap().id() != worktree_id); - cx.notify(); + self.remove_worktree(worktree_id, cx); Ok(()) } @@ -738,49 +1142,40 @@ impl Project { ) -> Result<()> { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { - worktree.update(cx, |worktree, cx| { - worktree - .as_remote_mut() - .unwrap() - .update_diagnostic_summary(envelope, 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(project_path)); + } } Ok(()) } fn handle_disk_based_diagnostics_updating( &mut self, - envelope: TypedEnvelope, + _: TypedEnvelope, _: Arc, cx: &mut ModelContext, ) -> Result<()> { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { - worktree.update(cx, |worktree, cx| { - worktree - .as_remote() - .unwrap() - .disk_based_diagnostics_updating(cx); - }); - } + self.disk_based_diagnostics_started(cx); Ok(()) } fn handle_disk_based_diagnostics_updated( &mut self, - envelope: TypedEnvelope, + _: TypedEnvelope, _: Arc, cx: &mut ModelContext, ) -> Result<()> { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { - worktree.update(cx, |worktree, cx| { - worktree - .as_remote() - .unwrap() - .disk_based_diagnostics_updated(cx); - }); - } + self.disk_based_diagnostics_finished(cx); Ok(()) } @@ -835,26 +1230,51 @@ impl Project { rpc: Arc, cx: &mut ModelContext, ) -> anyhow::Result<()> { + let receipt = envelope.receipt(); + let peer_id = envelope.original_sender_id()?; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { - return worktree.update(cx, |worktree, cx| { - worktree.handle_open_buffer(envelope, rpc, cx) - }); - } else { - Err(anyhow!("no such worktree")) - } + let worktree = self + .worktree_for_id(worktree_id, cx) + .ok_or_else(|| anyhow!("no such worktree"))?; + + let task = self.open_buffer( + ProjectPath { + worktree_id, + path: PathBuf::from(envelope.payload.path).into(), + }, + cx, + ); + cx.spawn(|_, mut cx| { + async move { + let buffer = task.await?; + let response = worktree.update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .open_remote_buffer(peer_id, buffer, cx) + }); + rpc.respond(receipt, response).await?; + Ok(()) + } + .log_err() + }) + .detach(); + Ok(()) } pub fn handle_close_buffer( &mut self, envelope: TypedEnvelope, - rpc: Arc, + _: Arc, cx: &mut ModelContext, ) -> anyhow::Result<()> { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); if let Some(worktree) = self.worktree_for_id(worktree_id, cx) { worktree.update(cx, |worktree, cx| { - worktree.handle_close_buffer(envelope, rpc, cx) + worktree + .as_local_mut() + .unwrap() + .close_remote_buffer(envelope, cx) })?; } Ok(()) @@ -884,10 +1304,13 @@ impl Project { cancel_flag: &'a AtomicBool, cx: &AppContext, ) -> impl 'a + Future> { - let include_root_name = self.worktrees.len() > 1; - let candidate_sets = self - .worktrees - .iter() + let worktrees = self + .worktrees(cx) + .filter(|worktree| !worktree.read(cx).is_weak()) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() .map(|worktree| CandidateSet { snapshot: worktree.read(cx).snapshot(), include_ignored, @@ -910,6 +1333,15 @@ impl Project { } } +impl WorktreeHandle { + pub fn upgrade(&self, cx: &AppContext) -> Option> { + match self { + WorktreeHandle::Strong(handle) => Some(handle.clone()), + WorktreeHandle::Weak(handle) => handle.upgrade(cx), + } + } +} + struct CandidateSet { snapshot: Snapshot, include_ignored: bool, @@ -997,6 +1429,25 @@ impl Entity for Project { } } } + + fn app_will_quit( + &mut self, + _: &mut MutableAppContext, + ) -> Option>>> { + use futures::FutureExt; + + let shutdown_futures = self + .language_servers + .drain() + .filter_map(|(_, server)| server.shutdown()) + .collect::>(); + Some( + async move { + futures::future::join_all(shutdown_futures).await; + } + .boxed(), + ) + } } impl Collaborator { @@ -1021,11 +1472,16 @@ impl Collaborator { #[cfg(test)] mod tests { - use super::*; + use super::{Event, *}; use client::test::FakeHttpClient; use fs::RealFs; - use gpui::TestAppContext; - use language::LanguageRegistry; + use futures::StreamExt; + use gpui::{test::subscribe, TestAppContext}; + use language::{ + tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageRegistry, + LanguageServerConfig, Point, + }; + use lsp::Url; use serde_json::json; use std::{os::unix, path::PathBuf}; use util::test::temp_tree; @@ -1057,9 +1513,9 @@ mod tests { let project = build_project(&mut cx); - let tree = project + let (tree, _) = project .update(&mut cx, |project, cx| { - project.add_local_worktree(&root_link_path, cx) + project.find_or_create_worktree_for_abs_path(&root_link_path, false, cx) }) .await .unwrap(); @@ -1093,6 +1549,139 @@ mod tests { ); } + #[gpui::test] + async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_server) = + LanguageServerConfig::fake(cx.background()).await; + let progress_token = language_server_config + .disk_based_diagnostics_progress_token + .clone() + .unwrap(); + + let mut languages = LanguageRegistry::new(); + languages.add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + let dir = temp_tree(json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = Client::new(http_client.clone()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + + let project = cx.update(|cx| { + Project::local( + client, + user_store, + Arc::new(languages), + Arc::new(RealFs), + cx, + ) + }); + + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(dir.path(), false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + // Cause worktree to start the fake language server + let _buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer( + ProjectPath { + worktree_id, + path: Path::new("b.rs").into(), + }, + cx, + ) + }) + .await + .unwrap(); + + let mut events = subscribe(&project, &mut cx); + + fake_server.start_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsStarted + ); + + fake_server.start_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + fake_server.start_progress(&progress_token).await; + + fake_server + .notify::(lsp::PublishDiagnosticsParams { + uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: "undefined variable 'A'".to_string(), + ..Default::default() + }], + }) + .await; + assert_eq!( + events.next().await.unwrap(), + Event::DiagnosticsUpdated(ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")) + }) + ); + + fake_server.end_progress(&progress_token).await; + fake_server.end_progress(&progress_token).await; + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsUpdated + ); + assert_eq!( + events.next().await.unwrap(), + Event::DiskBasedDiagnosticsFinished + ); + + let (buffer, _) = tree + .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) + .await + .unwrap(); + + buffer.read_with(&cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let diagnostics = snapshot + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(); + assert_eq!( + diagnostics, + &[DiagnosticEntry { + range: Point::new(0, 9)..Point::new(0, 10), + diagnostic: Diagnostic { + severity: lsp::DiagnosticSeverity::ERROR, + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, + ..Default::default() + } + }] + ) + }); + } + #[gpui::test] async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) { let dir = temp_tree(json!({ @@ -1105,9 +1694,9 @@ mod tests { })); let project = build_project(&mut cx); - let tree = project + let (tree, _) = project .update(&mut cx, |project, cx| { - project.add_local_worktree(&dir.path(), cx) + project.find_or_create_worktree_for_abs_path(&dir.path(), false, cx) }) .await .unwrap(); @@ -1125,6 +1714,126 @@ mod tests { assert!(results.is_empty()); } + #[gpui::test] + async fn test_definition(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_server) = + LanguageServerConfig::fake(cx.background()).await; + + let mut languages = LanguageRegistry::new(); + languages.add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + let dir = temp_tree(json!({ + "a.rs": "const fn a() { A }", + "b.rs": "const y: i32 = crate::a()", + })); + + let http_client = FakeHttpClient::with_404_response(); + let client = Client::new(http_client.clone()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); + let project = cx.update(|cx| { + Project::local( + client, + user_store, + Arc::new(languages), + Arc::new(RealFs), + cx, + ) + }); + + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(dir.path().join("b.rs"), false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + // Cause worktree to start the fake language server + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer( + ProjectPath { + worktree_id, + path: Path::new("").into(), + }, + cx, + ) + }) + .await + .unwrap(); + let definitions = + project.update(&mut cx, |project, cx| project.definition(&buffer, 22, cx)); + let (request_id, request) = fake_server + .receive_request::() + .await; + let request_params = request.text_document_position_params; + assert_eq!( + request_params.text_document.uri.to_file_path().unwrap(), + dir.path().join("b.rs") + ); + assert_eq!(request_params.position, lsp::Position::new(0, 22)); + + fake_server + .respond( + request_id, + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path(dir.path().join("a.rs")).unwrap(), + lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + ))), + ) + .await; + let mut definitions = definitions.await.unwrap(); + 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().abs_path(), + Some(dir.path().join("a.rs")) + ); + assert_eq!(definition.target_range.to_offset(target_buffer), 9..10); + assert_eq!( + list_worktrees(&project, cx), + [ + (dir.path().join("b.rs"), false), + (dir.path().join("a.rs"), true) + ] + ); + + drop(definition); + }); + cx.read(|cx| { + assert_eq!( + list_worktrees(&project, cx), + [(dir.path().join("b.rs"), false)] + ); + }); + + fn list_worktrees(project: &ModelHandle, cx: &AppContext) -> Vec<(PathBuf, bool)> { + project + .read(cx) + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + ( + worktree.as_local().unwrap().abs_path().to_path_buf(), + worktree.is_weak(), + ) + }) + .collect::>() + } + } + fn build_project(cx: &mut TestAppContext) -> ModelHandle { let languages = Arc::new(LanguageRegistry::new()); let fs = Arc::new(RealFs); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c01e660eab6cbe9687cc2a5fad22a5b22a9eb292..e0a54e45ed776e9cbdb0743e6254c0fa714a0d97 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4,7 +4,7 @@ use super::{ DiagnosticSummary, }; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; @@ -15,11 +15,10 @@ use gpui::{ Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ - range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language, - LanguageRegistry, Operation, PointUtf16, Rope, + range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Operation, + PointUtf16, Rope, }; use lazy_static::lazy_static; -use lsp::LanguageServer; use parking_lot::Mutex; use postage::{ prelude::{Sink as _, Stream as _}, @@ -37,7 +36,7 @@ use std::{ ops::Deref, path::{Path, PathBuf}, sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, SystemTime}, @@ -65,36 +64,24 @@ pub enum Worktree { Remote(RemoteWorktree), } -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Event { - DiskBasedDiagnosticsUpdating, - DiskBasedDiagnosticsUpdated, - DiagnosticsUpdated(Arc), -} - impl Entity for Worktree { - type Event = Event; + type Event = (); - fn app_will_quit( - &mut self, - _: &mut MutableAppContext, - ) -> Option>>> { - use futures::FutureExt; - - if let Self::Local(worktree) = self { - let shutdown_futures = worktree - .language_servers - .drain() - .filter_map(|(_, server)| server.shutdown()) - .collect::>(); - Some( - async move { - futures::future::join_all(shutdown_futures).await; - } - .boxed(), - ) - } else { - None + fn release(&mut self, cx: &mut MutableAppContext) { + if let Some(worktree) = self.as_local_mut() { + if let Registration::Done { project_id } = worktree.registration { + let client = worktree.client.clone(); + let unregister_message = proto::UnregisterWorktree { + project_id, + worktree_id: worktree.id().to_proto(), + }; + cx.foreground() + .spawn(async move { + client.send(unregister_message).await?; + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } } } } @@ -104,12 +91,12 @@ impl Worktree { client: Arc, user_store: ModelHandle, path: impl Into>, + weak: bool, fs: Arc, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { let (tree, scan_states_tx) = - LocalWorktree::new(client, user_store, path, fs.clone(), languages, cx).await?; + LocalWorktree::new(client, user_store, path, weak, fs.clone(), cx).await?; tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); let abs_path = tree.snapshot.abs_path.clone(); @@ -131,7 +118,6 @@ impl Worktree { worktree: proto::Worktree, client: Arc, user_store: ModelHandle, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { let remote_id = worktree.id; @@ -141,6 +127,7 @@ impl Worktree { .map(|c| c.to_ascii_lowercase()) .collect(); let root_name = worktree.root_name.clone(); + let weak = worktree.weak; let (entries_by_path, entries_by_id, diagnostic_summaries) = cx .background() .spawn(async move { @@ -238,9 +225,9 @@ impl Worktree { loading_buffers: Default::default(), open_buffers: Default::default(), queued_operations: Default::default(), - languages, user_store, diagnostic_summaries, + weak, }) }) }); @@ -280,6 +267,10 @@ impl Worktree { } } + pub fn is_local(&self) -> bool { + matches!(self, Worktree::Local(_)) + } + pub fn snapshot(&self) -> Snapshot { match self { Worktree::Local(worktree) => worktree.snapshot(), @@ -287,6 +278,13 @@ impl Worktree { } } + pub fn is_weak(&self) -> bool { + match self { + Worktree::Local(worktree) => worktree.weak, + Worktree::Remote(worktree) => worktree.weak, + } + } + pub fn replica_id(&self) -> ReplicaId { match self { Worktree::Local(_) => 0, @@ -306,13 +304,6 @@ impl Worktree { } } - pub fn languages(&self) -> &Arc { - match self { - Worktree::Local(worktree) => &worktree.language_registry, - Worktree::Remote(worktree) => &worktree.languages, - } - } - pub fn user_store(&self) -> &ModelHandle { match self { Worktree::Local(worktree) => &worktree.user_store, @@ -320,43 +311,6 @@ impl Worktree { } } - pub fn handle_open_buffer( - &mut self, - envelope: TypedEnvelope, - rpc: Arc, - cx: &mut ModelContext, - ) -> anyhow::Result<()> { - let receipt = envelope.receipt(); - - let response = self - .as_local_mut() - .unwrap() - .open_remote_buffer(envelope, cx); - - cx.background() - .spawn( - async move { - rpc.respond(receipt, response.await?).await?; - Ok(()) - } - .log_err(), - ) - .detach(); - - Ok(()) - } - - pub fn handle_close_buffer( - &mut self, - envelope: TypedEnvelope, - _: Arc, - cx: &mut ModelContext, - ) -> anyhow::Result<()> { - self.as_local_mut() - .unwrap() - .close_remote_buffer(envelope, cx) - } - pub fn diagnostic_summaries<'a>( &'a self, ) -> impl Iterator, DiagnosticSummary)> + 'a { @@ -379,7 +333,7 @@ impl Worktree { &mut self, path: impl AsRef, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task, bool)>> { let path = path.as_ref(); // If there is already a buffer for the given path, then return it. @@ -388,9 +342,10 @@ impl Worktree { Worktree::Remote(worktree) => worktree.get_open_buffer(path, cx), }; if let Some(existing_buffer) = existing_buffer { - return cx.spawn(move |_, _| async move { Ok(existing_buffer) }); + return cx.spawn(move |_, _| async move { Ok((existing_buffer, false)) }); } + let is_new = Arc::new(AtomicBool::new(true)); let path: Arc = Arc::from(path); let mut loading_watch = match self.loading_buffers().entry(path.clone()) { // If the given path is already being loaded, then wait for that existing @@ -412,7 +367,10 @@ impl Worktree { // After the buffer loads, record the fact that it is no longer // loading. this.update(&mut cx, |this, _| this.loading_buffers().remove(&path)); - *tx.borrow_mut() = Some(result.map_err(|e| Arc::new(e))); + *tx.borrow_mut() = Some(match result { + Ok(buffer) => Ok((buffer, is_new)), + Err(error) => Err(Arc::new(error)), + }); }) .detach(); rx @@ -422,7 +380,10 @@ impl Worktree { cx.spawn(|_, _| async move { loop { if let Some(result) = loading_watch.borrow().as_ref() { - return result.clone().map_err(|e| anyhow!("{}", e)); + return match result { + Ok((buf, is_new)) => Ok((buf.clone(), is_new.fetch_and(false, SeqCst))), + Err(error) => Err(anyhow!("{}", error)), + }; } loading_watch.recv().await; } @@ -526,7 +487,7 @@ impl Worktree { let worktree_id = envelope.payload.worktree_id; let buffer_id = envelope.payload.buffer_id; let save = cx.spawn(|_, mut cx| async move { - buffer.update(&mut cx, |buffer, cx| buffer.save(cx))?.await + buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await }); cx.background() @@ -731,179 +692,6 @@ impl Worktree { } } - pub fn update_diagnostics( - &mut self, - params: lsp::PublishDiagnosticsParams, - disk_based_sources: &HashSet, - cx: &mut ModelContext, - ) -> Result<()> { - let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?; - let abs_path = params - .uri - .to_file_path() - .map_err(|_| anyhow!("URI is not a file"))?; - let worktree_path = Arc::from( - abs_path - .strip_prefix(&this.abs_path) - .context("path is not within worktree")?, - ); - - let mut next_group_id = 0; - let mut diagnostics = Vec::default(); - let mut primary_diagnostic_group_ids = HashMap::default(); - let mut sources_by_group_id = HashMap::default(); - let mut supporting_diagnostic_severities = HashMap::default(); - for diagnostic in ¶ms.diagnostics { - let source = diagnostic.source.as_ref(); - let code = diagnostic.code.as_ref().map(|code| match code { - lsp::NumberOrString::Number(code) => code.to_string(), - lsp::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), - )) - }) - }); - - if is_supporting { - if let Some(severity) = diagnostic.severity { - supporting_diagnostic_severities - .insert((source, code.clone(), range), severity); - } - } else { - let group_id = post_inc(&mut next_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 { - 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, - }, - }); - if let Some(infos) = &diagnostic.related_information { - for info in infos { - if info.location.uri == params.uri { - let range = range_from_lsp(info.location.range); - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - code: code.clone(), - severity: DiagnosticSeverity::INFORMATION, - message: info.message.clone(), - group_id, - is_primary: false, - is_valid: true, - is_disk_based, - }, - }); - } - } - } - } - } - - 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) = supporting_diagnostic_severities.get(&( - source, - diagnostic.code.clone(), - entry.range.clone(), - )) { - diagnostic.severity = severity; - } - } - } - - self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; - Ok(()) - } - - pub fn update_diagnostic_entries( - &mut self, - worktree_path: Arc, - version: Option, - diagnostics: Vec>, - cx: &mut ModelContext, - ) -> Result<()> { - let this = self.as_local_mut().unwrap(); - for buffer in this.open_buffers.values() { - if let Some(buffer) = buffer.upgrade(cx) { - if buffer - .read(cx) - .file() - .map_or(false, |file| *file.path() == worktree_path) - { - let (remote_id, operation) = buffer.update(cx, |buffer, cx| { - ( - buffer.remote_id(), - buffer.update_diagnostics(version, diagnostics.clone(), cx), - ) - }); - self.send_buffer_update(remote_id, operation?, cx); - break; - } - } - } - - let this = self.as_local_mut().unwrap(); - let summary = DiagnosticSummary::new(&diagnostics); - this.diagnostic_summaries - .insert(PathKey(worktree_path.clone()), summary.clone()); - this.diagnostics.insert(worktree_path.clone(), diagnostics); - - cx.emit(Event::DiagnosticsUpdated(worktree_path.clone())); - - if let Some(share) = this.share.as_ref() { - cx.foreground() - .spawn({ - let client = this.client.clone(); - let project_id = share.project_id; - let worktree_id = this.id().to_proto(); - let path = worktree_path.to_string_lossy().to_string(); - async move { - client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id, - summary: Some(proto::DiagnosticSummary { - path, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - info_count: summary.info_count as u32, - hint_count: summary.hint_count as u32, - }), - }) - .await - .log_err() - } - }) - .detach(); - } - - Ok(()) - } - fn send_buffer_update( &mut self, buffer_id: u64, @@ -991,6 +779,7 @@ pub struct LocalWorktree { last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, poll_task: Option>, + registration: Registration, share: Option, loading_buffers: LoadingBuffers, open_buffers: HashMap>, @@ -998,12 +787,17 @@ pub struct LocalWorktree { diagnostics: HashMap, Vec>>, diagnostic_summaries: TreeMap, queued_operations: Vec<(u64, Operation)>, - language_registry: Arc, client: Arc, user_store: ModelHandle, fs: Arc, - languages: Vec>, - language_servers: HashMap>, + weak: bool, +} + +#[derive(Debug, Eq, PartialEq)] +enum Registration { + None, + Pending, + Done { project_id: u64 }, } struct ShareState { @@ -1021,15 +815,17 @@ pub struct RemoteWorktree { replica_id: ReplicaId, loading_buffers: LoadingBuffers, open_buffers: HashMap, - languages: Arc, user_store: ModelHandle, queued_operations: Vec<(u64, Operation)>, diagnostic_summaries: TreeMap, + weak: bool, } type LoadingBuffers = HashMap< Arc, - postage::watch::Receiver, Arc>>>, + postage::watch::Receiver< + Option, Arc), Arc>>, + >, >; #[derive(Default, Deserialize)] @@ -1042,8 +838,8 @@ impl LocalWorktree { client: Arc, user_store: ModelHandle, path: impl Into>, + weak: bool, fs: Arc, - languages: Arc, cx: &mut AsyncAppContext, ) -> Result<(ModelHandle, Sender)> { let abs_path = path.into(); @@ -1098,6 +894,7 @@ impl LocalWorktree { background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, + registration: Registration::None, share: None, poll_task: None, loading_buffers: Default::default(), @@ -1106,12 +903,10 @@ impl LocalWorktree { diagnostics: Default::default(), diagnostic_summaries: Default::default(), queued_operations: Default::default(), - language_registry: languages, client, user_store, fs, - languages: Default::default(), - language_servers: Default::default(), + weak, }; cx.spawn_weak(|this, mut cx| async move { @@ -1151,295 +946,299 @@ impl LocalWorktree { self.config.collaborators.clone() } - pub fn language_registry(&self) -> &LanguageRegistry { - &self.language_registry - } - - pub fn languages(&self) -> &[Arc] { - &self.languages + fn get_open_buffer( + &mut self, + path: &Path, + cx: &mut ModelContext, + ) -> Option> { + let handle = cx.handle(); + let mut result = None; + self.open_buffers.retain(|_buffer_id, buffer| { + if let Some(buffer) = buffer.upgrade(cx) { + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.worktree == handle && file.path().as_ref() == path { + result = Some(buffer); + } + } + true + } else { + false + } + }); + result } - pub fn register_language( + fn open_buffer( &mut self, - language: &Arc, + path: &Path, cx: &mut ModelContext, - ) -> Option> { - if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) { - self.languages.push(language.clone()); - } + ) -> Task>> { + let path = Arc::from(path); + cx.spawn(move |this, mut cx| async move { + let (file, contents) = this + .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) + .await?; - if let Some(server) = self.language_servers.get(language.name()) { - return Some(server.clone()); - } + let diagnostics = this.update(&mut cx, |this, _| { + this.as_local_mut().unwrap().diagnostics.get(&path).cloned() + }); - if let Some(language_server) = language - .start_server(self.abs_path(), cx) - .log_err() - .flatten() - { - enum DiagnosticProgress { - Updating, - Updated, - } + let mut buffer_operations = Vec::new(); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx); + if let Some(diagnostics) = diagnostics { + let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap(); + buffer_operations.push(op); + } + buffer + }); - let disk_based_sources = language - .disk_based_diagnostic_sources() - .cloned() - .unwrap_or_default(); - let disk_based_diagnostics_progress_token = - language.disk_based_diagnostics_progress_token().cloned(); - let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); - let (disk_based_diagnostics_done_tx, disk_based_diagnostics_done_rx) = - smol::channel::unbounded(); - language_server - .on_notification::(move |params| { - smol::block_on(diagnostics_tx.send(params)).ok(); - }) - .detach(); - cx.spawn_weak(|this, mut cx| { - let has_disk_based_diagnostic_progress_token = - disk_based_diagnostics_progress_token.is_some(); - let disk_based_diagnostics_done_tx = disk_based_diagnostics_done_tx.clone(); - async move { - while let Ok(diagnostics) = diagnostics_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - handle.update(&mut cx, |this, cx| { - if !has_disk_based_diagnostic_progress_token { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updating), - ) - .ok(); - } - this.update_diagnostics(diagnostics, &disk_based_sources, cx) - .log_err(); - if !has_disk_based_diagnostic_progress_token { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updated), - ) - .ok(); - } - }) - } else { - break; - } - } + this.update(&mut cx, |this, cx| { + for op in buffer_operations { + this.send_buffer_update(buffer.read(cx).remote_id(), op, cx); } - }) - .detach(); + let this = this.as_local_mut().unwrap(); + this.open_buffers.insert(buffer.id(), buffer.downgrade()); + }); - let mut pending_disk_based_diagnostics: i32 = 0; - language_server - .on_notification::(move |params| { - let token = match params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => Some(token), - }; + Ok(buffer) + }) + } - if token == disk_based_diagnostics_progress_token { - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => match progress { - lsp::WorkDoneProgress::Begin(_) => { - if pending_disk_based_diagnostics == 0 { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updating), - ) - .ok(); - } - pending_disk_based_diagnostics += 1; - } - lsp::WorkDoneProgress::End(_) => { - pending_disk_based_diagnostics -= 1; - if pending_disk_based_diagnostics == 0 { - smol::block_on( - disk_based_diagnostics_done_tx - .send(DiagnosticProgress::Updated), - ) - .ok(); - } - } - _ => {} - }, - } - } - }) - .detach(); - let rpc = self.client.clone(); - cx.spawn_weak(|this, mut cx| async move { - while let Ok(progress) = disk_based_diagnostics_done_rx.recv().await { - if let Some(handle) = cx.read(|cx| this.upgrade(cx)) { - match progress { - DiagnosticProgress::Updating => { - let message = handle.update(&mut cx, |this, cx| { - cx.emit(Event::DiskBasedDiagnosticsUpdating); - let this = this.as_local().unwrap(); - this.share.as_ref().map(|share| { - proto::DiskBasedDiagnosticsUpdating { - project_id: share.project_id, - worktree_id: this.id().to_proto(), - } - }) - }); - - if let Some(message) = message { - rpc.send(message).await.log_err(); - } - } - DiagnosticProgress::Updated => { - let message = handle.update(&mut cx, |this, cx| { - cx.emit(Event::DiskBasedDiagnosticsUpdated); - let this = this.as_local().unwrap(); - this.share.as_ref().map(|share| { - proto::DiskBasedDiagnosticsUpdated { - project_id: share.project_id, - worktree_id: this.id().to_proto(), - } - }) - }); - - if let Some(message) = message { - rpc.send(message).await.log_err(); - } - } - } - } else { - break; - } - } - }) - .detach(); + pub fn open_remote_buffer( + &mut self, + peer_id: PeerId, + buffer: ModelHandle, + cx: &mut ModelContext, + ) -> proto::OpenBufferResponse { + self.shared_buffers + .entry(peer_id) + .or_default() + .insert(buffer.id() as u64, buffer.clone()); + proto::OpenBufferResponse { + buffer: Some(buffer.update(cx.as_mut(), |buffer, _| buffer.to_proto())), + } + } - self.language_servers - .insert(language.name().to_string(), language_server.clone()); - Some(language_server.clone()) - } else { - None + pub fn close_remote_buffer( + &mut self, + envelope: TypedEnvelope, + cx: &mut ModelContext, + ) -> Result<()> { + if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) { + shared_buffers.remove(&envelope.payload.buffer_id); + cx.notify(); } + + Ok(()) } - fn get_open_buffer( + pub fn remove_collaborator( &mut self, - path: &Path, + peer_id: PeerId, + replica_id: ReplicaId, cx: &mut ModelContext, - ) -> Option> { - let handle = cx.handle(); - let mut result = None; - self.open_buffers.retain(|_buffer_id, buffer| { + ) { + self.shared_buffers.remove(&peer_id); + for (_, buffer) in &self.open_buffers { if let Some(buffer) = buffer.upgrade(cx) { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if file.worktree == handle && file.path().as_ref() == path { - result = Some(buffer); + buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); + } + } + cx.notify(); + } + + pub fn update_diagnostics( + &mut self, + worktree_path: Arc, + params: lsp::PublishDiagnosticsParams, + disk_based_sources: &HashSet, + cx: &mut ModelContext, + ) -> Result<()> { + let mut next_group_id = 0; + let mut diagnostics = Vec::default(); + let mut primary_diagnostic_group_ids = HashMap::default(); + let mut sources_by_group_id = HashMap::default(); + let mut supporting_diagnostic_severities = HashMap::default(); + for diagnostic in ¶ms.diagnostics { + let source = diagnostic.source.as_ref(); + let code = diagnostic.code.as_ref().map(|code| match code { + lsp::NumberOrString::Number(code) => code.to_string(), + lsp::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), + )) + }) + }); + + if is_supporting { + if let Some(severity) = diagnostic.severity { + supporting_diagnostic_severities + .insert((source, code.clone(), range), severity); + } + } else { + let group_id = post_inc(&mut next_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 { + 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, + }, + }); + if let Some(infos) = &diagnostic.related_information { + for info in infos { + if info.location.uri == params.uri { + let range = range_from_lsp(info.location.range); + diagnostics.push(DiagnosticEntry { + range, + diagnostic: Diagnostic { + code: code.clone(), + severity: DiagnosticSeverity::INFORMATION, + message: info.message.clone(), + group_id, + is_primary: false, + is_valid: true, + is_disk_based, + }, + }); + } } } - true - } else { - false } - }); - result - } - - fn open_buffer( - &mut self, - path: &Path, - cx: &mut ModelContext, - ) -> Task>> { - let path = Arc::from(path); - cx.spawn(move |this, mut cx| async move { - let (file, contents) = this - .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) - .await?; - - let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - let diagnostics = this.diagnostics.get(&path).cloned(); - let language = this - .language_registry - .select_language(file.full_path()) - .cloned(); - let server = language - .as_ref() - .and_then(|language| this.register_language(language, cx)); - (diagnostics, language, server) - }); - - let mut buffer_operations = Vec::new(); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx); - buffer.set_language(language, language_server, cx); - if let Some(diagnostics) = diagnostics { - let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap(); - buffer_operations.push(op); - } - buffer - }); + } - this.update(&mut cx, |this, cx| { - for op in buffer_operations { - this.send_buffer_update(buffer.read(cx).remote_id(), op, cx); + 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) = supporting_diagnostic_severities.get(&( + source, + diagnostic.code.clone(), + entry.range.clone(), + )) { + diagnostic.severity = severity; } - let this = this.as_local_mut().unwrap(); - this.open_buffers.insert(buffer.id(), buffer.downgrade()); - }); + } + } - Ok(buffer) - }) + self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?; + Ok(()) } - pub fn open_remote_buffer( + pub fn update_diagnostic_entries( &mut self, - envelope: TypedEnvelope, + worktree_path: Arc, + version: Option, + diagnostics: Vec>, cx: &mut ModelContext, - ) -> Task> { - cx.spawn(|this, mut cx| async move { - let peer_id = envelope.original_sender_id(); - let path = Path::new(&envelope.payload.path); - let buffer = this - .update(&mut cx, |this, cx| this.open_buffer(path, cx)) - .await?; - this.update(&mut cx, |this, cx| { - this.as_local_mut() - .unwrap() - .shared_buffers - .entry(peer_id?) - .or_default() - .insert(buffer.id() as u64, buffer.clone()); + ) -> Result<()> { + for buffer in self.open_buffers.values() { + if let Some(buffer) = buffer.upgrade(cx) { + if buffer + .read(cx) + .file() + .map_or(false, |file| *file.path() == worktree_path) + { + let (remote_id, operation) = buffer.update(cx, |buffer, cx| { + ( + buffer.remote_id(), + buffer.update_diagnostics(version, diagnostics.clone(), cx), + ) + }); + self.send_buffer_update(remote_id, operation?, cx); + break; + } + } + } - Ok(proto::OpenBufferResponse { - buffer: Some(buffer.update(cx.as_mut(), |buffer, _| buffer.to_proto())), - }) - }) - }) - } + let summary = DiagnosticSummary::new(&diagnostics); + self.diagnostic_summaries + .insert(PathKey(worktree_path.clone()), summary.clone()); + self.diagnostics.insert(worktree_path.clone(), diagnostics); - pub fn close_remote_buffer( - &mut self, - envelope: TypedEnvelope, - cx: &mut ModelContext, - ) -> Result<()> { - if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) { - shared_buffers.remove(&envelope.payload.buffer_id); - cx.notify(); + if let Some(share) = self.share.as_ref() { + cx.foreground() + .spawn({ + let client = self.client.clone(); + let project_id = share.project_id; + let worktree_id = self.id().to_proto(); + let path = worktree_path.to_string_lossy().to_string(); + async move { + client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id, + summary: Some(proto::DiagnosticSummary { + path, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + info_count: summary.info_count as u32, + hint_count: summary.hint_count as u32, + }), + }) + .await + .log_err() + } + }) + .detach(); } Ok(()) } - pub fn remove_collaborator( + fn send_buffer_update( &mut self, - peer_id: PeerId, - replica_id: ReplicaId, + buffer_id: u64, + operation: Operation, cx: &mut ModelContext, - ) { - self.shared_buffers.remove(&peer_id); - for (_, buffer) in &self.open_buffers { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); + ) -> Option<()> { + let share = self.share.as_ref()?; + let project_id = share.project_id; + let worktree_id = self.id(); + let rpc = self.client.clone(); + cx.spawn(|worktree, mut cx| async move { + if let Err(error) = rpc + .request(proto::UpdateBuffer { + project_id, + worktree_id: worktree_id.0 as u64, + buffer_id, + operations: vec![language::proto::serialize_operation(&operation)], + }) + .await + { + worktree.update(&mut cx, |worktree, _| { + log::error!("error sending buffer operation: {}", error); + worktree + .as_local_mut() + .unwrap() + .queued_operations + .push((buffer_id, operation)); + }); } - } - cx.notify(); + }) + .detach(); + None } pub fn scan_complete(&self) -> impl Future { @@ -1508,26 +1307,35 @@ impl LocalWorktree { pub fn save_buffer_as( &self, - buffer: ModelHandle, + buffer_handle: ModelHandle, path: impl Into>, - text: Rope, cx: &mut ModelContext, - ) -> Task> { + ) -> Task> { + let buffer = buffer_handle.read(cx); + let text = buffer.as_rope().clone(); + let version = buffer.version(); let save = self.save(path, text, cx); cx.spawn(|this, mut cx| async move { let entry = save.await?; - this.update(&mut cx, |this, cx| { + let file = this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); - this.open_buffers.insert(buffer.id(), buffer.downgrade()); - Ok(File { + this.open_buffers + .insert(buffer_handle.id(), buffer_handle.downgrade()); + File { entry_id: Some(entry.id), worktree: cx.handle(), worktree_path: this.abs_path.clone(), path: entry.path, mtime: entry.mtime, is_local: true, - }) - }) + } + }); + + buffer_handle.update(&mut cx, |buffer, cx| { + buffer.did_save(version, file.mtime, Some(Box::new(file)), cx); + }); + + Ok(()) }) } @@ -1553,11 +1361,48 @@ impl LocalWorktree { }) } - pub fn share( + pub fn register( &mut self, project_id: u64, cx: &mut ModelContext, ) -> Task> { + if self.registration != Registration::None { + return Task::ready(Ok(())); + } + + self.registration = Registration::Pending; + let client = self.client.clone(); + let register_message = proto::RegisterWorktree { + project_id, + worktree_id: self.id().to_proto(), + root_name: self.root_name().to_string(), + authorized_logins: self.authorized_logins(), + }; + cx.spawn(|this, mut cx| async move { + let response = client.request(register_message).await; + this.update(&mut cx, |this, _| { + let worktree = this.as_local_mut().unwrap(); + match response { + Ok(_) => { + worktree.registration = Registration::Done { project_id }; + Ok(()) + } + Err(error) => { + worktree.registration = Registration::None; + Err(error) + } + } + }) + }) + } + + pub fn share(&mut self, cx: &mut ModelContext) -> Task> { + let project_id = if let Registration::Done { project_id } = self.registration { + project_id + } else { + return Task::ready(Err(anyhow!("cannot share worktree before registering it"))); + }; + if self.share.is_some() { return Task::ready(Ok(())); } @@ -1588,10 +1433,11 @@ impl LocalWorktree { }); let diagnostic_summaries = self.diagnostic_summaries.clone(); + let weak = self.weak; let share_message = cx.background().spawn(async move { proto::ShareWorktree { project_id, - worktree: Some(snapshot.to_proto(&diagnostic_summaries)), + worktree: Some(snapshot.to_proto(&diagnostic_summaries, weak)), } }); @@ -1713,16 +1559,10 @@ impl RemoteWorktree { mtime: entry.mtime, is_local: false, }; - let language = this.read_with(&cx, |this, _| { - use language::File; - this.languages().select_language(file.full_path()).cloned() - }); let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?; let buffer_id = remote_buffer.id as usize; let buffer = cx.add_model(|cx| { - Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx) - .unwrap() - .with_language(language, None, cx) + Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx).unwrap() }); this.update(&mut cx, move |this, cx| { let this = this.as_remote_mut().unwrap(); @@ -1769,30 +1609,18 @@ impl RemoteWorktree { pub fn update_diagnostic_summary( &mut self, - envelope: TypedEnvelope, - cx: &mut ModelContext, + path: Arc, + summary: &proto::DiagnosticSummary, ) { - if let Some(summary) = envelope.payload.summary { - let path: Arc = Path::new(&summary.path).into(); - self.diagnostic_summaries.insert( - PathKey(path.clone()), - DiagnosticSummary { - error_count: summary.error_count as usize, - warning_count: summary.warning_count as usize, - info_count: summary.info_count as usize, - hint_count: summary.hint_count as usize, - }, - ); - cx.emit(Event::DiagnosticsUpdated(path)); - } - } - - pub fn disk_based_diagnostics_updating(&self, cx: &mut ModelContext) { - cx.emit(Event::DiskBasedDiagnosticsUpdating); - } - - pub fn disk_based_diagnostics_updated(&self, cx: &mut ModelContext) { - cx.emit(Event::DiskBasedDiagnosticsUpdated); + self.diagnostic_summaries.insert( + PathKey(path.clone()), + DiagnosticSummary { + error_count: summary.error_count as usize, + warning_count: summary.warning_count as usize, + info_count: summary.info_count as usize, + hint_count: summary.hint_count as usize, + }, + ); } pub fn remove_collaborator(&mut self, replica_id: ReplicaId, cx: &mut ModelContext) { @@ -1827,6 +1655,7 @@ impl Snapshot { pub fn to_proto( &self, diagnostic_summaries: &TreeMap, + weak: bool, ) -> proto::Worktree { let root_name = self.root_name.clone(); proto::Worktree { @@ -1842,6 +1671,7 @@ impl Snapshot { .iter() .map(|(path, summary)| summary.to_proto(path.0.clone())) .collect(), + weak, } } @@ -2202,7 +2032,7 @@ impl fmt::Debug for Snapshot { #[derive(Clone, PartialEq)] pub struct File { entry_id: Option, - worktree: ModelHandle, + pub worktree: ModelHandle, worktree_path: Arc, pub path: Arc, pub mtime: SystemTime, @@ -3257,9 +3087,7 @@ mod tests { use anyhow::Result; use client::test::{FakeHttpClient, FakeServer}; use fs::RealFs; - use gpui::test::subscribe; - use language::{tree_sitter_rust, DiagnosticEntry, LanguageServerConfig}; - use language::{Diagnostic, LanguageConfig}; + use language::{Diagnostic, DiagnosticEntry}; use lsp::Url; use rand::prelude::*; use serde_json::json; @@ -3296,8 +3124,8 @@ mod tests { client, user_store, Arc::from(Path::new("/root")), + false, Arc::new(fs), - Default::default(), &mut cx.to_async(), ) .await @@ -3334,19 +3162,19 @@ mod tests { client, user_store, dir.path(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await .unwrap(); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file1", cx)) .await .unwrap(); let save = buffer.update(&mut cx, |buffer, cx| { buffer.edit(Some(0..0), "a line of text.\n".repeat(10 * 1024), cx); - buffer.save(cx).unwrap() + buffer.save(cx) }); save.await.unwrap(); @@ -3369,8 +3197,8 @@ mod tests { client, user_store, file_path.clone(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3379,13 +3207,13 @@ mod tests { .await; cx.read(|cx| assert_eq!(tree.read(cx).file_count(), 1)); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("", cx)) .await .unwrap(); let save = buffer.update(&mut cx, |buffer, cx| { buffer.edit(Some(0..0), "a line of text.\n".repeat(10 * 1024), cx); - buffer.save(cx).unwrap() + buffer.save(cx) }); save.await.unwrap(); @@ -3418,8 +3246,8 @@ mod tests { client, user_store.clone(), dir.path(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3427,7 +3255,7 @@ mod tests { let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { let buffer = tree.update(cx, |tree, cx| tree.open_buffer(path, cx)); - async move { buffer.await.unwrap() } + async move { buffer.await.unwrap().0 } }; let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { tree.read_with(cx, |tree, _| { @@ -3455,10 +3283,9 @@ mod tests { let remote = Worktree::remote( 1, 1, - initial_snapshot.to_proto(&Default::default()), + initial_snapshot.to_proto(&Default::default(), Default::default()), Client::new(http_client.clone()), user_store, - Default::default(), &mut cx.to_async(), ) .await @@ -3570,8 +3397,8 @@ mod tests { client, user_store, dir.path(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3623,8 +3450,8 @@ mod tests { client.clone(), user_store, "/the-dir".as_ref(), + false, fs, - Default::default(), &mut cx.to_async(), ) .await @@ -3639,9 +3466,9 @@ mod tests { ) }); - 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(); + let buffer_a_1 = buffer_a_1.await.unwrap().0; + let buffer_a_2 = buffer_a_2.await.unwrap().0; + let buffer_b = buffer_b.await.unwrap().0; 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"); @@ -3654,7 +3481,8 @@ mod tests { let buffer_a_3 = worktree .update(&mut cx, |worktree, cx| worktree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; // There's still only one buffer per path. assert_eq!(buffer_a_3.id(), buffer_a_id); @@ -3677,8 +3505,8 @@ mod tests { client, user_store, dir.path(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3687,7 +3515,7 @@ mod tests { cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - let buffer1 = tree + let (buffer1, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file1", cx)) .await .unwrap(); @@ -3754,7 +3582,7 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); - let buffer2 = tree + let (buffer2, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file2", cx)) .await .unwrap(); @@ -3775,7 +3603,7 @@ mod tests { // 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 = tree + let (buffer3, _) = tree .update(&mut cx, |tree, cx| tree.open_buffer("file3", cx)) .await .unwrap(); @@ -3814,8 +3642,8 @@ mod tests { client, user_store, dir.path(), + false, Arc::new(RealFs), - Default::default(), &mut cx.to_async(), ) .await @@ -3824,7 +3652,7 @@ mod tests { .await; let abs_path = dir.path().join("the-file"); - let buffer = tree + let (buffer, _) = tree .update(&mut cx, |tree, cx| { tree.open_buffer(Path::new("the-file"), cx) }) @@ -3903,115 +3731,6 @@ mod tests { .await; } - #[gpui::test] - async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) { - let (language_server_config, mut fake_server) = - LanguageServerConfig::fake(cx.background()).await; - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); - - let dir = temp_tree(json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", - })); - - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - - let tree = Worktree::open_local( - client, - user_store, - dir.path(), - Arc::new(RealFs), - Arc::new(languages), - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Cause worktree to start the fake language server - let _buffer = tree - .update(&mut cx, |tree, cx| tree.open_buffer("b.rs", cx)) - .await - .unwrap(); - - let mut events = subscribe(&tree, &mut cx); - - fake_server.start_progress(&progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsUpdating - ); - - fake_server.start_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - fake_server.start_progress(&progress_token).await; - - fake_server - .notify::(lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), - version: None, - diagnostics: vec![lsp::Diagnostic { - range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), - severity: Some(lsp::DiagnosticSeverity::ERROR), - message: "undefined variable 'A'".to_string(), - ..Default::default() - }], - }) - .await; - assert_eq!( - events.next().await.unwrap(), - Event::DiagnosticsUpdated(Arc::from(Path::new("a.rs"))) - ); - - fake_server.end_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - assert_eq!( - events.next().await.unwrap(), - Event::DiskBasedDiagnosticsUpdated - ); - - let buffer = tree - .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) - .await - .unwrap(); - - buffer.read_with(&cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let diagnostics = snapshot - .diagnostics_in_range::<_, Point>(0..buffer.len()) - .collect::>(); - assert_eq!( - diagnostics, - &[DiagnosticEntry { - range: Point::new(0, 9)..Point::new(0, 10), - diagnostic: Diagnostic { - severity: lsp::DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string(), - group_id: 0, - is_primary: true, - ..Default::default() - } - }] - ) - }); - } - #[gpui::test] async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { let fs = Arc::new(FakeFs::new()); @@ -4038,14 +3757,14 @@ mod tests { client.clone(), user_store, "/the-dir".as_ref(), + false, fs, - Default::default(), &mut cx.to_async(), ) .await .unwrap(); - let buffer = worktree + let (buffer, _) = worktree .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx)) .await .unwrap(); @@ -4152,7 +3871,12 @@ mod tests { worktree .update(&mut cx, |tree, cx| { - tree.update_diagnostics(message, &Default::default(), cx) + tree.as_local_mut().unwrap().update_diagnostics( + Arc::from("a.rs".as_ref()), + message, + &Default::default(), + cx, + ) }) .unwrap(); let buffer = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 382a94284991d6d49775ffc988b73d9ae081d705..781c4cd586a88300b5aebc8e6885600557b82ac9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6,8 +6,8 @@ use gpui::{ }, keymap::{self, Binding}, platform::CursorStyle, - AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, - ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, + ViewHandle, WeakViewHandle, }; use postage::watch; use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId}; @@ -24,7 +24,7 @@ use workspace::{ pub struct ProjectPanel { project: ModelHandle, list: UniformListState, - visible_entries: Vec>, + visible_entries: Vec<(WorktreeId, Vec)>, expanded_dir_ids: HashMap>, selection: Option, settings: watch::Receiver, @@ -260,7 +260,11 @@ impl ProjectPanel { } fn select_first(&mut self, cx: &mut ViewContext) { - if let Some(worktree) = self.project.read(cx).worktrees().first() { + let worktree = self + .visible_entries + .first() + .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); + if let Some(worktree) = worktree { let worktree = worktree.read(cx); let worktree_id = worktree.id(); if let Some(root_entry) = worktree.root_entry() { @@ -289,10 +293,11 @@ impl ProjectPanel { let project = self.project.read(cx); let mut offset = None; let mut ix = 0; - for (worktree_ix, visible_entries) in self.visible_entries.iter().enumerate() { + for (worktree_id, visible_entries) in &self.visible_entries { if target_ix < ix + visible_entries.len() { - let worktree = project.worktrees()[worktree_ix].read(cx); - offset = Some((worktree, visible_entries[target_ix - ix])); + offset = project + .worktree_for_id(*worktree_id, cx) + .map(|w| (w.read(cx), visible_entries[target_ix - ix])); break; } else { ix += visible_entries.len(); @@ -318,7 +323,11 @@ impl ProjectPanel { new_selected_entry: Option<(WorktreeId, usize)>, cx: &mut ViewContext, ) { - let worktrees = self.project.read(cx).worktrees(); + let worktrees = self + .project + .read(cx) + .worktrees(cx) + .filter(|worktree| !worktree.read(cx).is_weak()); self.visible_entries.clear(); let mut entry_ix = 0; @@ -369,7 +378,8 @@ impl ProjectPanel { } entry_iter.advance(); } - self.visible_entries.push(visible_worktree_entries); + self.visible_entries + .push((worktree_id, visible_worktree_entries)); } } @@ -404,16 +414,14 @@ impl ProjectPanel { } } - fn for_each_visible_entry( + fn for_each_visible_entry( &self, range: Range, - cx: &mut C, - mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C), + cx: &mut ViewContext, + mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext), ) { - let project = self.project.read(cx); - let worktrees = project.worktrees().to_vec(); let mut ix = 0; - for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { + for (worktree_id, visible_worktree_entries) in &self.visible_entries { if ix >= range.end { return; } @@ -423,37 +431,38 @@ impl ProjectPanel { } let end_ix = range.end.min(ix + visible_worktree_entries.len()); - let worktree = &worktrees[worktree_ix]; - let snapshot = worktree.read(cx).snapshot(); - let expanded_entry_ids = self - .expanded_dir_ids - .get(&snapshot.id()) - .map(Vec::as_slice) - .unwrap_or(&[]); - let root_name = OsStr::new(snapshot.root_name()); - let mut cursor = snapshot.entries(false); - - for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix] - .iter() - .copied() - { - cursor.advance_to_offset(ix); - if let Some(entry) = cursor.entry() { - let filename = entry.path.file_name().unwrap_or(root_name); - let details = EntryDetails { - filename: filename.to_string_lossy().to_string(), - depth: entry.path.components().count(), - is_dir: entry.is_dir(), - is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), - is_selected: self.selection.map_or(false, |e| { - e.worktree_id == snapshot.id() && e.entry_id == entry.id - }), - }; - let entry = ProjectEntry { - worktree_id: snapshot.id(), - entry_id: entry.id, - }; - callback(entry, details, cx); + if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let snapshot = worktree.read(cx).snapshot(); + let expanded_entry_ids = self + .expanded_dir_ids + .get(&snapshot.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); + let root_name = OsStr::new(snapshot.root_name()); + let mut cursor = snapshot.entries(false); + + for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix] + .iter() + .copied() + { + cursor.advance_to_offset(ix); + if let Some(entry) = cursor.entry() { + let filename = entry.path.file_name().unwrap_or(root_name); + let details = EntryDetails { + filename: filename.to_string_lossy().to_string(), + depth: entry.path.components().count(), + is_dir: entry.is_dir(), + is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), + is_selected: self.selection.map_or(false, |e| { + e.worktree_id == snapshot.id() && e.entry_id == entry.id + }), + }; + let entry = ProjectEntry { + worktree_id: snapshot.id(), + entry_id: entry.id, + }; + callback(entry, details, cx); + } } } ix = end_ix; @@ -545,7 +554,7 @@ impl View for ProjectPanel { self.list.clone(), self.visible_entries .iter() - .map(|worktree_entries| worktree_entries.len()) + .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), move |range, items, cx| { let theme = &settings.borrow().theme.project_panel; @@ -633,18 +642,18 @@ mod tests { cx, ) }); - let root1 = project + let (root1, _) = project .update(&mut cx, |project, cx| { - project.add_local_worktree("/root1", cx) + project.find_or_create_worktree_for_abs_path("/root1", false, cx) }) .await .unwrap(); root1 .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete()) .await; - let root2 = project + let (root2, _) = project .update(&mut cx, |project, cx| { - project.add_local_worktree("/root2", cx) + project.find_or_create_worktree_for_abs_path("/root2", false, cx) }) .await .unwrap(); @@ -827,7 +836,7 @@ mod tests { ) { let path = path.as_ref(); panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees() { + for worktree in panel.project.read(cx).worktrees(cx).collect::>() { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 47774bf360f92dcac041b561540c40d20483245b..9891ce21cffafeaa3e7d622ac86d01a30f195cae 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -191,12 +191,10 @@ message DiagnosticSummary { message DiskBasedDiagnosticsUpdating { uint64 project_id = 1; - uint64 worktree_id = 2; } message DiskBasedDiagnosticsUpdated { uint64 project_id = 1; - uint64 worktree_id = 2; } message GetChannels {} @@ -274,6 +272,7 @@ message Worktree { string root_name = 2; repeated Entry entries = 3; repeated DiagnosticSummary diagnostic_summaries = 4; + bool weak = 5; } message Entry { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0c1f6a52873de6f96f765dc6befd92aea16741eb..3ad10421363ff31eb1f68ecc6c52783aa3f94e22 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -309,6 +309,7 @@ impl Server { .values() .cloned() .collect(), + weak: worktree.weak, }) }) .collect(); @@ -421,6 +422,7 @@ impl Server { authorized_user_ids: contact_user_ids.clone(), root_name: request.payload.root_name, share: None, + weak: false, }, ); @@ -1158,8 +1160,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -1184,7 +1188,7 @@ mod tests { ) .await .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let replica_id_b = project_b.read_with(&cx_b, |project, _| { assert_eq!( @@ -1213,7 +1217,8 @@ mod tests { let buffer_b = worktree_b .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); buffer_b.read_with(&cx_b, |buf, cx| { assert_eq!(buf.read(cx).text(), "b-contents") @@ -1222,7 +1227,8 @@ mod tests { let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let editor_b = cx_b.add_view(window_b, |cx| { Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), cx) @@ -1291,8 +1297,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -1319,7 +1327,7 @@ mod tests { .await .unwrap(); - let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap()); worktree_b .update(&mut cx_b, |tree, cx| tree.open_buffer("a.txt", cx)) .await @@ -1351,7 +1359,7 @@ mod tests { ) .await .unwrap(); - let worktree_c = project_c.read_with(&cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_c = project_c.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap()); worktree_c .update(&mut cx_b, |tree, cx| tree.open_buffer("a.txt", cx)) .await @@ -1393,8 +1401,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -1431,16 +1441,18 @@ mod tests { .unwrap(); // Open and edit a buffer as both guests B and C. - let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone()); - let worktree_c = project_c.read_with(&cx_c, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap()); + let worktree_c = project_c.read_with(&cx_c, |p, cx| p.worktrees(cx).next().unwrap()); let buffer_b = worktree_b .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; let buffer_c = worktree_c .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx)); @@ -1448,7 +1460,8 @@ mod tests { let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx)) .await - .unwrap(); + .unwrap() + .0; buffer_a .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ") @@ -1469,7 +1482,7 @@ mod tests { .await; // Edit the buffer as the host and concurrently save as guest B. - let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap()); + let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx)); buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx)); save_b.await.unwrap(); assert_eq!( @@ -1542,8 +1555,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/dir", false, cx) + }) .await .unwrap(); worktree_a @@ -1568,13 +1583,14 @@ mod tests { ) .await .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); // Open a buffer as client B let buffer_b = worktree_b .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime()); buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx)); @@ -1585,7 +1601,6 @@ mod tests { buffer_b .update(&mut cx_b, |buf, cx| buf.save(cx)) - .unwrap() .await .unwrap(); worktree_b @@ -1637,8 +1652,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/dir", false, cx) + }) .await .unwrap(); worktree_a @@ -1663,13 +1680,14 @@ mod tests { ) .await .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); // Open a buffer as client A let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) .await - .unwrap(); + .unwrap() + .0; // Start opening the same buffer as client B let buffer_b = cx_b @@ -1681,7 +1699,7 @@ mod tests { buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); - let buffer_b = buffer_b.await.unwrap(); + let buffer_b = buffer_b.await.unwrap().0; buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; } @@ -1717,8 +1735,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/dir", false, cx) + }) .await .unwrap(); worktree_a @@ -1743,7 +1763,7 @@ mod tests { ) .await .unwrap(); - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); // See that a guest has joined as client A. project_a @@ -1793,8 +1813,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -1880,8 +1902,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -1899,8 +1923,14 @@ mod tests { // Cause the language server to start. let _ = cx_a .background() - .spawn(worktree_a.update(&mut cx_a, |worktree, cx| { - worktree.open_buffer("other.rs", cx) + .spawn(project_a.update(&mut cx_a, |project, cx| { + project.open_buffer( + ProjectPath { + worktree_id, + path: Path::new("other.rs").into(), + }, + cx, + ) })) .await .unwrap(); @@ -2011,12 +2041,13 @@ mod tests { .await; // Open the file with the errors on client B. They should be present. - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let buffer_b = cx_b .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) .await - .unwrap(); + .unwrap() + .0; buffer_b.read_with(&cx_b, |buffer, _| { assert_eq!( @@ -2095,8 +2126,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a @@ -2123,12 +2156,13 @@ mod tests { .unwrap(); // Open the file to be formatted on client B. - let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let buffer_b = cx_b .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx))) .await - .unwrap(); + .unwrap() + .0; let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx)); let (request_id, _) = fake_language_server @@ -2602,8 +2636,10 @@ mod tests { cx, ) }); - let worktree_a = project_a - .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_worktree_for_abs_path("/a", false, cx) + }) .await .unwrap(); worktree_a diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index a115b27db651e2f707443af50fc90456bd55fd93..b7aec2689bd5b575ee335775f881356a8fa09207 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -31,6 +31,7 @@ pub struct Worktree { pub authorized_user_ids: Vec, pub root_name: String, pub share: Option, + pub weak: bool, } #[derive(Default)] @@ -202,6 +203,7 @@ impl Store { let mut worktree_root_names = project .worktrees .values() + .filter(|worktree| !worktree.weak) .map(|worktree| worktree.root_name.clone()) .collect::>(); worktree_root_names.sort_unstable(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index be0e3f05fbefc80ebad7064b6ae8175415f93be7..17f370ac4fa574dc3c088b46191bd19942089f31 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -76,14 +76,16 @@ pub struct Pane { item_views: Vec<(usize, Box)>, active_item_index: usize, settings: watch::Receiver, - navigation: Rc, + nav_history: Rc>, } -#[derive(Default)] -pub struct Navigation(RefCell); +pub struct ItemNavHistory { + history: Rc>, + item_view: Rc, +} #[derive(Default)] -struct NavigationHistory { +pub struct NavHistory { mode: NavigationMode, backward_stack: VecDeque, forward_stack: VecDeque, @@ -104,7 +106,7 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item_view: Box, + pub item_view: Rc, pub data: Option>, } @@ -114,7 +116,7 @@ impl Pane { item_views: Vec::new(), active_item_index: 0, settings, - navigation: Default::default(), + nav_history: Default::default(), } } @@ -148,7 +150,7 @@ impl Pane { ) -> Task<()> { let to_load = pane.update(cx, |pane, cx| { // Retrieve the weak item handle from the history. - let entry = pane.navigation.pop(mode)?; + let entry = pane.nav_history.borrow_mut().pop(mode)?; // If the item is still present in this pane, then activate it. if let Some(index) = entry @@ -157,9 +159,11 @@ impl Pane { .and_then(|v| pane.index_for_item_view(v.as_ref())) { if let Some(item_view) = pane.active_item() { - pane.navigation.set_mode(mode); + pane.nav_history.borrow_mut().set_mode(mode); item_view.deactivated(cx); - pane.navigation.set_mode(NavigationMode::Normal); + pane.nav_history + .borrow_mut() + .set_mode(NavigationMode::Normal); } pane.active_item_index = index; @@ -173,8 +177,7 @@ impl Pane { // If the item is no longer present in this pane, then retrieve its // project path in order to reopen it. else { - pane.navigation - .0 + pane.nav_history .borrow_mut() .paths_by_item .get(&entry.item_view.id()) @@ -192,9 +195,11 @@ impl Pane { if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) { if let Some(item) = item.log_err() { workspace.update(&mut cx, |workspace, cx| { - pane.update(cx, |p, _| p.navigation.set_mode(mode)); + pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode)); let item_view = workspace.open_item_in_pane(item, &pane, cx); - pane.update(cx, |p, _| p.navigation.set_mode(NavigationMode::Normal)); + pane.update(cx, |p, _| { + p.nav_history.borrow_mut().set_mode(NavigationMode::Normal) + }); if let Some(data) = entry.data { item_view.navigate(data, cx); @@ -232,7 +237,7 @@ impl Pane { } let item_view = - item_handle.add_view(cx.window_id(), workspace, self.navigation.clone(), cx); + item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx); self.add_item_view(item_view.boxed_clone(), cx); item_view } @@ -322,11 +327,11 @@ impl Pane { item_view.deactivated(cx); } - let mut navigation = self.navigation.0.borrow_mut(); + let mut nav_history = self.nav_history.borrow_mut(); if let Some(path) = item_view.project_path(cx) { - navigation.paths_by_item.insert(item_view.id(), path); + nav_history.paths_by_item.insert(item_view.id(), path); } else { - navigation.paths_by_item.remove(&item_view.id()); + nav_history.paths_by_item.remove(&item_view.id()); } item_ix += 1; @@ -349,7 +354,7 @@ impl Pane { fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { - cx.focus(active_item.to_any()); + cx.focus(active_item); } } @@ -536,16 +541,33 @@ impl View for Pane { } } -impl Navigation { - pub fn pop_backward(&self) -> Option { - self.0.borrow_mut().backward_stack.pop_back() +impl ItemNavHistory { + pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { + Self { + history, + item_view: Rc::new(item_view.downgrade()), + } } - pub fn pop_forward(&self) -> Option { - self.0.borrow_mut().forward_stack.pop_back() + pub fn history(&self) -> Rc> { + self.history.clone() } - fn pop(&self, mode: NavigationMode) -> Option { + pub fn push(&self, data: Option) { + self.history.borrow_mut().push(data, self.item_view.clone()); + } +} + +impl NavHistory { + pub fn pop_backward(&mut self) -> Option { + self.backward_stack.pop_back() + } + + pub fn pop_forward(&mut self) -> Option { + self.forward_stack.pop_back() + } + + fn pop(&mut self, mode: NavigationMode) -> Option { match mode { NavigationMode::Normal => None, NavigationMode::GoingBack => self.pop_backward(), @@ -553,38 +575,41 @@ impl Navigation { } } - fn set_mode(&self, mode: NavigationMode) { - self.0.borrow_mut().mode = mode; + fn set_mode(&mut self, mode: NavigationMode) { + self.mode = mode; } - pub fn push(&self, data: Option, cx: &mut ViewContext) { - let mut state = self.0.borrow_mut(); - match state.mode { + pub fn push( + &mut self, + data: Option, + item_view: Rc, + ) { + match self.mode { NavigationMode::Normal => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); + if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + self.backward_stack.pop_front(); } - state.backward_stack.push_back(NavigationEntry { - item_view: Box::new(cx.weak_handle()), + self.backward_stack.push_back(NavigationEntry { + item_view, data: data.map(|data| Box::new(data) as Box), }); - state.forward_stack.clear(); + self.forward_stack.clear(); } NavigationMode::GoingBack => { - if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.forward_stack.pop_front(); + if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + self.forward_stack.pop_front(); } - state.forward_stack.push_back(NavigationEntry { - item_view: Box::new(cx.weak_handle()), + self.forward_stack.push_back(NavigationEntry { + item_view, data: data.map(|data| Box::new(data) as Box), }); } NavigationMode::GoingForward => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); + if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { + self.backward_stack.pop_front(); } - state.backward_stack.push_back(NavigationEntry { - item_view: Box::new(cx.weak_handle()), + self.backward_stack.push_back(NavigationEntry { + item_view, data: data.map(|data| Box::new(data) as Box), }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e1f21237613b56a7c8289c80d585fe70b60b6deb..ef91d4b015c8dfdc47fad81550cb9e1f7321f37f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -33,7 +33,8 @@ use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItem use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ - any::Any, + any::{Any, TypeId}, + cell::RefCell, future::Future, hash::{Hash, Hasher}, path::{Path, PathBuf}, @@ -66,7 +67,11 @@ pub fn init(cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); - cx.add_action(Workspace::save_active_item); + cx.add_action( + |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { + workspace.save_active_item(cx).detach_and_log_err(cx); + }, + ); cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); @@ -125,9 +130,9 @@ pub struct JoinProjectParams { pub trait PathOpener { fn open( &self, - worktree: &mut Worktree, + project: &mut Project, path: ProjectPath, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Option>>>; } @@ -137,7 +142,7 @@ pub trait Item: Entity + Sized { fn build_view( handle: ModelHandle, workspace: &Workspace, - navigation: Rc, + nav_history: ItemNavHistory, cx: &mut ViewContext, ) -> Self::View; @@ -165,14 +170,14 @@ pub trait ItemView: View { false } fn can_save(&self, cx: &AppContext) -> bool; - fn save(&mut self, cx: &mut ViewContext) -> Result>>; + fn save(&mut self, cx: &mut ViewContext) -> Task>; fn can_save_as(&self, cx: &AppContext) -> bool; fn save_as( &mut self, - worktree: ModelHandle, - path: &Path, + project: ModelHandle, + abs_path: PathBuf, cx: &mut ViewContext, - ) -> Task>; + ) -> Task>; fn should_activate_item_on_event(_: &Self::Event) -> bool { false } @@ -182,6 +187,18 @@ pub trait ItemView: View { fn should_update_tab_on_event(_: &Self::Event) -> bool { false } + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &AppContext, + ) -> Option { + if TypeId::of::() == type_id { + Some(self_handle.into()) + } else { + None + } + } } pub trait ItemHandle: Send + Sync { @@ -190,7 +207,7 @@ pub trait ItemHandle: Send + Sync { &self, window_id: usize, workspace: &Workspace, - navigation: Rc, + nav_history: Rc>, cx: &mut MutableAppContext, ) -> Box; fn boxed_clone(&self) -> Box; @@ -204,7 +221,7 @@ pub trait WeakItemHandle { fn upgrade(&self, cx: &AppContext) -> Option>; } -pub trait ItemViewHandle { +pub trait ItemViewHandle: 'static { fn item_handle(&self, cx: &AppContext) -> Box; fn title(&self, cx: &AppContext) -> String; fn project_path(&self, cx: &AppContext) -> Option; @@ -219,13 +236,14 @@ pub trait ItemViewHandle { fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; fn can_save_as(&self, cx: &AppContext) -> bool; - fn save(&self, cx: &mut MutableAppContext) -> Result>>; + fn save(&self, cx: &mut MutableAppContext) -> Task>; fn save_as( &self, - worktree: ModelHandle, - path: &Path, + project: ModelHandle, + abs_path: PathBuf, cx: &mut MutableAppContext, - ) -> Task>; + ) -> Task>; + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; } pub trait WeakItemViewHandle { @@ -242,11 +260,12 @@ impl ItemHandle for ModelHandle { &self, window_id: usize, workspace: &Workspace, - navigation: Rc, + nav_history: Rc>, cx: &mut MutableAppContext, ) -> Box { Box::new(cx.add_view(window_id, |cx| { - T::build_view(self.clone(), workspace, navigation, cx) + let nav_history = ItemNavHistory::new(nav_history, &cx.handle()); + T::build_view(self.clone(), workspace, nav_history, cx) })) } @@ -276,10 +295,10 @@ impl ItemHandle for Box { &self, window_id: usize, workspace: &Workspace, - navigation: Rc, + nav_history: Rc>, cx: &mut MutableAppContext, ) -> Box { - ItemHandle::add_view(self.as_ref(), window_id, workspace, navigation, cx) + ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx) } fn boxed_clone(&self) -> Box { @@ -323,6 +342,17 @@ impl PartialEq for Box { impl Eq for Box {} +impl dyn ItemViewHandle { + pub fn downcast(&self) -> Option> { + self.to_any().downcast() + } + + pub fn act_as(&self, cx: &AppContext) -> Option> { + self.act_as_type(TypeId::of::(), cx) + .and_then(|t| t.downcast()) + } +} + impl ItemViewHandle for ViewHandle { fn item_handle(&self, cx: &AppContext) -> Box { Box::new(self.read(cx).item_handle(cx)) @@ -374,17 +404,17 @@ impl ItemViewHandle for ViewHandle { self.update(cx, |this, cx| this.navigate(data, cx)); } - fn save(&self, cx: &mut MutableAppContext) -> Result>> { + fn save(&self, cx: &mut MutableAppContext) -> Task> { self.update(cx, |item, cx| item.save(cx)) } fn save_as( &self, - worktree: ModelHandle, - path: &Path, + project: ModelHandle, + abs_path: PathBuf, cx: &mut MutableAppContext, ) -> Task> { - self.update(cx, |item, cx| item.save_as(worktree, path, cx)) + self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } fn is_dirty(&self, cx: &AppContext) -> bool { @@ -410,6 +440,16 @@ impl ItemViewHandle for ViewHandle { fn can_save_as(&self, cx: &AppContext) -> bool { self.read(cx).can_save_as(cx) } + + fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { + self.read(cx).act_as_type(type_id, self, cx) + } +} + +impl Into for Box { + fn into(self) -> AnyViewHandle { + self.to_any() + } } impl Clone for Box { @@ -600,8 +640,11 @@ impl Workspace { &self.project } - pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle] { - &self.project.read(cx).worktrees() + pub fn worktrees<'a>( + &self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.project.read(cx).worktrees(cx) } pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { @@ -621,7 +664,6 @@ impl Workspace { pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { let futures = self .worktrees(cx) - .iter() .filter_map(|worktree| worktree.read(cx).as_local()) .map(|worktree| worktree.scan_complete()) .collect::>(); @@ -675,44 +717,14 @@ impl Workspace { }) } - fn worktree_for_abs_path( - &self, - abs_path: &Path, - cx: &mut ViewContext, - ) -> Task, PathBuf)>> { - let abs_path: Arc = Arc::from(abs_path); - cx.spawn(|this, mut cx| async move { - let mut entry_id = None; - this.read_with(&cx, |this, cx| { - for tree in this.worktrees(cx) { - if let Some(relative_path) = tree - .read(cx) - .as_local() - .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok()) - { - entry_id = Some((tree.clone(), relative_path.into())); - break; - } - } - }); - - if let Some(entry_id) = entry_id { - Ok(entry_id) - } else { - let worktree = this - .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx)) - .await?; - Ok((worktree, PathBuf::new())) - } - }) - } - fn project_path_for_path( &self, abs_path: &Path, cx: &mut ViewContext, ) -> Task> { - let entry = self.worktree_for_abs_path(abs_path, cx); + let entry = self.project().update(cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(abs_path, false, cx) + }); cx.spawn(|_, cx| async move { let (worktree, path) = entry.await?; Ok(ProjectPath { @@ -722,15 +734,6 @@ impl Workspace { }) } - pub fn add_worktree( - &self, - path: &Path, - cx: &mut ViewContext, - ) -> Task>> { - self.project - .update(cx, |project, cx| project.add_local_worktree(path, cx)) - } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, add_view: F) where V: 'static + View, @@ -785,18 +788,11 @@ impl Workspace { return Task::ready(Ok(existing_item)); } - let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) { - Some(worktree) => worktree, - None => { - return Task::ready(Err(anyhow!("worktree {} does not exist", path.worktree_id))); - } - }; - let project_path = path.clone(); let path_openers = self.path_openers.clone(); - worktree.update(cx, |worktree, cx| { + self.project.update(cx, |project, cx| { for opener in path_openers.iter() { - if let Some(task) = opener.open(worktree, project_path.clone(), cx) { + if let Some(task) = opener.open(project, project_path.clone(), cx) { return task; } } @@ -825,70 +821,46 @@ impl Workspace { self.active_item(cx).and_then(|item| item.project_path(cx)) } - pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext) { + pub fn save_active_item(&mut self, cx: &mut ViewContext) -> Task> { if let Some(item) = self.active_item(cx) { - let handle = cx.handle(); if item.can_save(cx) { if item.has_conflict(cx.as_ref()) { const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - cx.prompt( + let mut answer = cx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, &["Overwrite", "Cancel"], - move |answer, cx| { - if answer == 0 { - cx.spawn(|mut cx| async move { - if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await - { - error!("failed to save item: {:?}, ", error); - } - }) - .detach(); - } - }, ); - } else { cx.spawn(|_, mut cx| async move { - if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await { - error!("failed to save item: {:?}, ", error); + let answer = answer.recv().await; + if answer == Some(0) { + cx.update(|cx| item.save(cx)).await?; } + Ok(()) }) - .detach(); + } else { + item.save(cx) } } else if item.can_save_as(cx) { - let worktree = self.worktrees(cx).first(); + let worktree = self.worktrees(cx).next(); let start_abs_path = worktree .and_then(|w| w.read(cx).as_local()) .map_or(Path::new(""), |w| w.abs_path()) .to_path_buf(); - cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| { - if let Some(abs_path) = abs_path { - cx.spawn(|mut cx| async move { - let result = match handle - .update(&mut cx, |this, cx| { - this.worktree_for_abs_path(&abs_path, cx) - }) - .await - { - Ok((worktree, path)) => { - handle - .update(&mut cx, |_, cx| { - item.save_as(worktree, &path, cx.as_mut()) - }) - .await - } - Err(error) => Err(error), - }; - - if let Err(error) = result { - error!("failed to save item: {:?}, ", error); - } - }) - .detach() + let mut abs_path = cx.prompt_for_new_path(&start_abs_path); + cx.spawn(|this, mut cx| async move { + if let Some(abs_path) = abs_path.recv().await.flatten() { + let project = this.read_with(&cx, |this, _| this.project().clone()); + cx.update(|cx| item.save_as(project, abs_path, cx)).await?; } - }); + Ok(()) + }) + } else { + Task::ready(Ok(())) } + } else { + Task::ready(Ok(())) } } @@ -1348,7 +1320,6 @@ impl WorkspaceHandle for ViewHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec { self.read(cx) .worktrees(cx) - .iter() .flat_map(|worktree| { let worktree_id = worktree.read(cx).id(); worktree.read(cx).files(true, 0).map(move |f| ProjectPath { @@ -1438,18 +1409,17 @@ impl std::fmt::Debug for OpenParams { fn open(action: &Open, cx: &mut MutableAppContext) { let app_state = action.0.clone(); - cx.prompt_for_paths( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - }, - move |paths, cx| { - if let Some(paths) = paths { - cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })); - } - }, - ); + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + cx.spawn(|mut cx| async move { + if let Some(paths) = paths.recv().await.flatten() { + cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }))); + } + }) + .detach(); } pub fn open_paths( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f5aeec861ace4634aeefb03e7316042a43c5e168..5f5422d25671708138fcc7ee697f4fd0203bfdd0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -175,7 +175,7 @@ mod tests { assert_eq!(cx.window_ids().len(), 1); let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); workspace_1.read_with(&cx, |workspace, cx| { - assert_eq!(workspace.worktrees(cx).len(), 2) + assert_eq!(workspace.worktrees(cx).count(), 2) }); cx.update(|cx| { @@ -205,7 +205,6 @@ mod tests { workspace .active_item(cx) .unwrap() - .to_any() .downcast::() .unwrap() }); @@ -214,18 +213,13 @@ mod tests { assert!(editor.text(cx).is_empty()); }); - workspace.update(&mut cx, |workspace, cx| { - workspace.save_active_item(&workspace::Save, cx) - }); - + let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); app_state.fs.as_fake().insert_dir("/root").await.unwrap(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); - - editor - .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name") - .await; - editor.update(&mut cx, |editor, cx| { + save_task.await.unwrap(); + editor.read_with(&cx, |editor, cx| { assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name"); }); } @@ -248,9 +242,10 @@ mod tests { .await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); @@ -360,9 +355,10 @@ mod tests { let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree("/dir1".as_ref(), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/dir1"), false, cx) }) .await .unwrap(); @@ -396,7 +392,6 @@ mod tests { let worktree_roots = workspace .read(cx) .worktrees(cx) - .iter() .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); assert_eq!( @@ -427,9 +422,10 @@ mod tests { let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); @@ -444,7 +440,7 @@ mod tests { let editor = cx.read(|cx| { let pane = workspace.read(cx).active_pane().read(cx); let item = pane.active_item().unwrap(); - item.to_any().downcast::().unwrap() + item.downcast::().unwrap() }); cx.update(|cx| { @@ -460,12 +456,13 @@ mod tests { .await; cx.read(|cx| assert!(editor.is_dirty(cx))); - cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&workspace::Save, cx))); + let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); cx.simulate_prompt_answer(window_id, 0); - editor - .condition(&cx, |editor, cx| !editor.is_dirty(cx)) - .await; - cx.read(|cx| assert!(!editor.has_conflict(cx))); + save_task.await.unwrap(); + editor.read_with(&cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + }); } #[gpui::test] @@ -474,21 +471,14 @@ mod tests { app_state.fs.as_fake().insert_dir("/root").await.unwrap(); let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); - let worktree = cx.read(|cx| { - workspace - .read(cx) - .worktrees(cx) - .iter() - .next() - .unwrap() - .clone() - }); + let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone())); @@ -496,7 +486,6 @@ mod tests { workspace .active_item(cx) .unwrap() - .to_any() .downcast::() .unwrap() }); @@ -513,9 +502,7 @@ mod tests { }); // Save the buffer. This prompts for a filename. - workspace.update(&mut cx, |workspace, cx| { - workspace.save_active_item(&workspace::Save, cx) - }); + let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); cx.simulate_new_path_selection(|parent_dir| { assert_eq!(parent_dir, Path::new("/root")); Some(parent_dir.join("the-new-name.rs")) @@ -525,17 +512,13 @@ mod tests { assert_eq!(editor.title(cx), "untitled"); }); - // When the save completes, the buffer's title is updated. - editor - .condition(&cx, |editor, cx| !editor.is_dirty(cx)) - .await; - cx.read(|cx| { + // When the save completes, the buffer's title is updated and the language is assigned based + // on the path. + save_task.await.unwrap(); + editor.read_with(&cx, |editor, cx| { assert!(!editor.is_dirty(cx)); assert_eq!(editor.title(cx), "the-new-name.rs"); - }); - // The language is assigned based on the path - editor.read_with(&cx, |editor, cx| { - assert_eq!(editor.language(cx).unwrap().name(), "Rust") + assert_eq!(editor.language(cx).unwrap().name(), "Rust"); }); // Edit the file and save it again. This time, there is no filename prompt. @@ -543,14 +526,13 @@ mod tests { editor.handle_input(&editor::Input(" there".into()), cx); assert_eq!(editor.is_dirty(cx.as_ref()), true); }); - workspace.update(&mut cx, |workspace, cx| { - workspace.save_active_item(&workspace::Save, cx) - }); + let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); + save_task.await.unwrap(); assert!(!cx.did_prompt_for_new_path()); - editor - .condition(&cx, |editor, cx| !editor.is_dirty(cx)) - .await; - cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs")); + editor.read_with(&cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); + assert_eq!(editor.title(cx), "the-new-name.rs") + }); // Open the same newly-created file in another pane item. The new editor should reuse // the same buffer. @@ -572,7 +554,6 @@ mod tests { workspace .active_item(cx) .unwrap() - .to_any() .downcast::() .unwrap() }); @@ -597,7 +578,6 @@ mod tests { workspace .active_item(cx) .unwrap() - .to_any() .downcast::() .unwrap() }); @@ -612,17 +592,12 @@ mod tests { }); // Save the buffer. This prompts for a filename. - workspace.update(&mut cx, |workspace, cx| { - workspace.save_active_item(&workspace::Save, cx) - }); + let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); - - editor - .condition(&cx, |editor, cx| !editor.is_dirty(cx)) - .await; - - // The language is assigned based on the path + save_task.await.unwrap(); + // The buffer is not dirty anymore and the language is assigned based on the path. editor.read_with(&cx, |editor, cx| { + assert!(!editor.is_dirty(cx)); assert_eq!(editor.language(cx).unwrap().name(), "Rust") }); } @@ -648,9 +623,10 @@ mod tests { let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); @@ -710,9 +686,10 @@ mod tests { .await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); - workspace - .update(&mut cx, |workspace, cx| { - workspace.add_worktree(Path::new("/root"), cx) + params + .project + .update(&mut cx, |project, cx| { + project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx) }) .await .unwrap(); @@ -727,7 +704,6 @@ mod tests { .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx)) .await .unwrap() - .to_any() .downcast::() .unwrap(); editor1.update(&mut cx, |editor, cx| { @@ -737,14 +713,12 @@ mod tests { .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx)) .await .unwrap() - .to_any() .downcast::() .unwrap(); let editor3 = workspace .update(&mut cx, |w, cx| w.open_path(file3.clone(), cx)) .await .unwrap() - .to_any() .downcast::() .unwrap(); editor3.update(&mut cx, |editor, cx| { @@ -860,7 +834,7 @@ mod tests { ) -> (ProjectPath, DisplayPoint) { workspace.update(cx, |workspace, cx| { let item = workspace.active_item(cx).unwrap(); - let editor = item.to_any().downcast::().unwrap(); + let editor = item.downcast::().unwrap(); let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)); (item.project_path(cx).unwrap(), selections[0].start) })