Detailed changes
@@ -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<Editor>,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
- paths_to_update: HashMap<WorktreeId, BTreeSet<ProjectPath>>,
+ paths_to_update: BTreeSet<ProjectPath>,
build_settings: BuildSettings,
settings: watch::Receiver<workspace::Settings>,
}
@@ -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::<Editor>()
.unwrap();
editor.update(cx, |editor, cx| {
@@ -522,10 +524,19 @@ impl workspace::Item for ProjectDiagnostics {
fn build_view(
handle: ModelHandle<Self>,
workspace: &Workspace,
- _: Rc<Navigation>,
+ nav_history: ItemNavHistory,
cx: &mut ViewContext<Self::View>,
) -> 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<project::ProjectPath> {
@@ -548,6 +559,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
None
}
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
+ 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<Self>) -> Result<Task<Result<()>>> {
+ fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
}
@@ -570,8 +586,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
fn save_as(
&mut self,
- _: ModelHandle<project::Worktree>,
- _: &std::path::Path,
+ _: ModelHandle<Project>,
+ _: PathBuf,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
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<Self>,
+ _: &AppContext,
+ ) -> Option<AnyViewHandle> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.into())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some((&self.editor).into())
+ } else {
+ None
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ 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;
@@ -19,7 +19,7 @@ impl DiagnosticSummary {
cx: &mut ViewContext<Self>,
) -> 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();
}
@@ -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));
@@ -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<Box<dyn PathOpene
Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
Binding::new("f8", ShowNextDiagnostic, Some("Editor")),
+ Binding::new("f12", GoToDefinition, Some("Editor")),
Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
Binding::new("pageup", PageUp, Some("Editor")),
Binding::new("pagedown", PageDown, Some("Editor")),
@@ -277,6 +278,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
cx.add_action(Editor::select_smaller_syntax_node);
cx.add_action(Editor::move_to_enclosing_bracket);
cx.add_action(Editor::show_next_diagnostic);
+ cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::page_up);
cx.add_action(Editor::page_down);
cx.add_action(Editor::fold);
@@ -379,7 +381,7 @@ pub struct Editor {
mode: EditorMode,
placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>,
- navigation: Option<Rc<Navigation>>,
+ nav_history: Option<ItemNavHistory>,
}
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<Workspace>,
) {
- 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<ItemNavHistory>) {
+ 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<Point>,
cx: &mut ViewContext<Self>,
) {
- 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<Workspace>,
+ ) {
+ let active_item = workspace.active_item(cx);
+ let editor_handle = if let Some(editor) = active_item
+ .as_ref()
+ .and_then(|item| item.act_as::<Self>(cx))
+ {
+ editor
+ } else {
+ return;
+ };
+
+ let editor = editor_handle.read(cx);
+ let buffer = editor.buffer.read(cx);
+ let head = editor.newest_selection::<usize>(&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::<Self>()
+ .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<Editor>) {
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))
@@ -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<Buffer>);
impl PathOpener for BufferOpener {
fn open(
&self,
- worktree: &mut Worktree,
+ project: &mut Project,
project_path: ProjectPath,
- cx: &mut ModelContext<Worktree>,
+ cx: &mut ModelContext<Project>,
) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
- 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<dyn ItemHandle>)
@@ -46,7 +46,7 @@ impl ItemHandle for BufferItemHandle {
&self,
window_id: usize,
workspace: &Workspace,
- navigation: Rc<Navigation>,
+ nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> {
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<Self>) {
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<Self>) -> Result<Task<Result<()>>> {
+ fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
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<Worktree>,
- path: &Path,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
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<Self>,
) {
- if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
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<Self>,
) {
- if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
self.update(editor, cx);
} else {
@@ -789,6 +789,19 @@ impl MultiBuffer {
cx.notify();
}
+ pub fn text_anchor_for_position<'a, T: ToOffset>(
+ &'a self,
+ position: T,
+ cx: &AppContext,
+ ) -> (ModelHandle<Buffer>, 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<Buffer>,
@@ -812,18 +825,18 @@ impl MultiBuffer {
})
}
- pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
+ pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
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<Language>> {
@@ -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();
@@ -80,17 +80,13 @@ impl GoToLine {
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
- workspace.toggle_modal(cx, |cx, workspace| {
- let editor = workspace
- .active_item(cx)
- .unwrap()
- .to_any()
- .downcast::<Editor>()
- .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::<Editor>() {
+ 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<Self>) {
@@ -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::<platform::test::Window>()
.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<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type ReleaseObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
pub struct MutableAppContext {
weak_self: Option<rc::Weak<RefCell<Self>>>,
@@ -674,6 +675,7 @@ pub struct MutableAppContext {
next_subscription_id: usize,
subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>,
observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>,
+ release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
presenters_and_platform_windows:
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> 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<F>(
+ 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<usize> {
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<F>(&self, options: PathPromptOptions, done_fn: F)
- where
- F: 'static + FnOnce(Option<Vec<PathBuf>>, &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<Option<Vec<PathBuf>>> {
+ self.foreground_platform.prompt_for_paths(options)
}
- pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
- where
- F: 'static + FnOnce(Option<PathBuf>, &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<Option<PathBuf>> {
+ self.foreground_platform.prompt_for_new_path(directory)
}
pub fn subscribe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1071,6 +1039,27 @@ impl MutableAppContext {
observations: Some(Arc::downgrade(&self.observations)),
}
}
+
+ pub fn observe_release<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ E::Event: 'static,
+ H: Handle<E>,
+ 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<S, F>(
+ &mut self,
+ handle: &ModelHandle<S>,
+ mut callback: F,
+ ) -> Subscription
+ where
+ S: Entity,
+ F: 'static + FnMut(&mut T, &mut ModelContext<T>),
+ {
+ 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<T> {
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<F>(&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<usize> {
+ self.app.prompt(self.window_id, level, msg, answers)
}
- pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
- where
- F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
- {
- self.app.prompt_for_paths(options, done_fn)
+ pub fn prompt_for_paths(
+ &self,
+ options: PathPromptOptions,
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+ self.app.prompt_for_paths(options)
}
- pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
- where
- F: 'static + FnOnce(Option<PathBuf>, &mut MutableAppContext),
- {
- self.app.prompt_for_new_path(directory, done_fn)
+ pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+ 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<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
+ where
+ E: Entity,
+ H: Handle<E>,
+ F: 'static + FnMut(&mut T, &mut ViewContext<T>),
+ {
+ 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<Weak<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>>,
},
+ ReleaseObservation {
+ id: usize,
+ entity_id: usize,
+ observations:
+ Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
+ },
}
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<Mutex<bool>>,
+ released: Rc<Cell<bool>>,
}
struct View {
- released: Arc<Mutex<bool>>,
+ released: Rc<Cell<bool>>,
}
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)]
@@ -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<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
- );
- fn prompt_for_new_path(
- &self,
- directory: &Path,
- done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
- );
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
+ fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
}
pub trait Dispatcher: Send + Sync {
@@ -89,13 +85,7 @@ pub trait Window: WindowContext {
fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
- fn prompt(
- &self,
- level: PromptLevel,
- msg: &str,
- answers: &[&str],
- done_fn: Box<dyn FnOnce(usize)>,
- );
+ fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self);
}
@@ -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<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
- ) {
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
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<dyn FnOnce(Option<std::path::PathBuf>)>,
- ) {
+ fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
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
}
}
}
@@ -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<dyn FnOnce(usize)>,
- ) {
+ ) -> oneshot::Receiver<usize> {
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
}
}
@@ -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<Option<(PathBuf, Box<dyn FnOnce(Option<PathBuf>)>)>>,
+ last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>,
}
struct Dispatcher;
@@ -35,7 +36,7 @@ pub struct Window {
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
- pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
+ pub(crate) last_prompt: Cell<Option<oneshot::Sender<usize>>>,
}
impl ForegroundPlatform {
@@ -43,11 +44,11 @@ impl ForegroundPlatform {
&self,
result: impl FnOnce(PathBuf) -> Option<PathBuf>,
) {
- 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<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
- ) {
+ ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+ let (_done_tx, done_rx) = oneshot::channel();
+ done_rx
}
- fn prompt_for_new_path(&self, path: &Path, f: Box<dyn FnOnce(Option<std::path::PathBuf>)>) {
- *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<Option<PathBuf>> {
+ 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<dyn FnOnce(usize)>) {
- self.last_prompt.replace(Some(f));
+ fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
+ let (done_tx, done_rx) = oneshot::channel();
+ self.last_prompt.replace(Some(done_tx));
+ done_rx
}
fn activate(&self) {}
@@ -55,7 +55,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.await;
if let Some(Some(Ok(item))) = opened.first() {
- if let Some(editor) = item.to_any().downcast::<Editor>() {
+ if let Some(editor) = item.downcast::<Editor>() {
editor.update(&mut cx, |editor, cx| {
let len = editor.buffer().read(cx).read(cx).len();
editor.select_ranges([len..len], Some(Autoscroll::Center), cx);
@@ -385,13 +385,17 @@ impl Buffer {
}
}
- pub fn with_language(
+ pub fn with_language(mut self, language: Arc<Language>, cx: &mut ModelContext<Self>) -> Self {
+ self.set_language(Some(language), cx);
+ self
+ }
+
+ pub fn with_language_server(
mut self,
- language: Option<Arc<Language>>,
- language_server: Option<Arc<LanguageServer>>,
+ server: Arc<LanguageServer>,
cx: &mut ModelContext<Self>,
) -> 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<Self>,
- ) -> Result<Task<Result<(clock::Global, SystemTime)>>> {
- let file = self
- .file
- .as_ref()
- .ok_or_else(|| anyhow!("buffer has no file"))?;
+ ) -> Task<Result<(clock::Global, SystemTime)>> {
+ 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<Arc<Language>>, cx: &mut ModelContext<Self>) {
+ self.language = language;
+ self.reparse(cx);
}
- pub fn set_language(
+ pub fn set_language_server(
&mut self,
- language: Option<Arc<Language>>,
language_server: Option<Arc<lsp::LanguageServer>>,
cx: &mut ModelContext<Self>,
) {
- 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();
}
@@ -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,
@@ -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<Self>, root_uri: lsp_types::Url) -> Result<()> {
+ async fn init(self: Arc<Self>, 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::<lsp_types::request::Initialize>(
+ let request = Self::request_internal::<request::Initialize>(
&this.next_id,
&this.response_handlers,
this.outbound_tx.read().as_ref(),
params,
);
request.await?;
- Self::notify_internal::<lsp_types::notification::Initialized>(
+ Self::notify_internal::<notification::Initialized>(
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::<lsp_types::request::Shutdown>(
+ Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
outbound_tx.as_ref(),
(),
)
.await?;
- Self::notify_internal::<lsp_types::notification::Exit>(outbound_tx.as_ref(), ())?;
+ Self::notify_internal::<notification::Exit>(outbound_tx.as_ref(), ())?;
drop(outbound_tx);
output_done.recv().await;
drop(tasks);
@@ -285,7 +291,7 @@ impl LanguageServer {
pub fn on_notification<T, F>(&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<T: lsp_types::request::Request>(
- self: Arc<Self>,
+ pub fn request<T: request::Request>(
+ self: &Arc<Self>,
params: T::Params,
) -> impl Future<Output = Result<T::Result>>
where
@@ -329,7 +335,7 @@ impl LanguageServer {
}
}
- fn request_internal<T: lsp_types::request::Request>(
+ fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
@@ -376,7 +382,7 @@ impl LanguageServer {
}
}
- pub fn notify<T: lsp_types::notification::Notification>(
+ pub fn notify<T: notification::Notification>(
self: &Arc<Self>,
params: T::Params,
) -> impl Future<Output = Result<()>> {
@@ -388,7 +394,7 @@ impl LanguageServer {
}
}
- fn notify_internal<T: lsp_types::notification::Notification>(
+ fn notify_internal<T: notification::Notification>(
outbound_tx: Option<&channel::Sender<Vec<u8>>>,
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::notification::DidOpenTextDocument>(
- lsp_types::DidOpenTextDocumentParams {
- text_document: lsp_types::TextDocumentItem::new(
- lib_file_uri.clone(),
- "rust".to_string(),
- 0,
- lib_source,
- ),
- },
- )
+ .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
+ text_document: TextDocumentItem::new(
+ lib_file_uri.clone(),
+ "rust".to_string(),
+ 0,
+ lib_source,
+ ),
+ })
.await
.unwrap();
let hover = server
- .request::<lsp_types::request::HoverRequest>(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::<request::HoverRequest>(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::<lsp_types::request::Shutdown>().await;
+ let (shutdown_request, _) = fake.receive_request::<request::Shutdown>().await;
fake.respond(shutdown_request, ()).await;
- fake.receive_notification::<lsp_types::notification::Exit>()
- .await;
+ fake.receive_notification::<notification::Exit>().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";
}
@@ -144,7 +144,7 @@ impl OutlineView {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
if let Some(editor) = workspace
.active_item(cx)
- .and_then(|item| item.to_any().downcast::<Editor>())
+ .and_then(|item| item.downcast::<Editor>())
{
let settings = workspace.settings();
let buffer = editor
@@ -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<ModelHandle<Worktree>>,
+ worktrees: Vec<WorktreeHandle>,
active_entry: Option<ProjectEntry>,
languages: Arc<LanguageRegistry>,
+ language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
client: Arc<client::Client>,
user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>,
client_state: ProjectClientState,
collaborators: HashMap<PeerId, Collaborator>,
subscriptions: Vec<client::Subscription>,
- pending_disk_based_diagnostics: isize,
+ language_servers_with_diagnostics_running: isize,
+}
+
+enum WorktreeHandle {
+ Strong(ModelHandle<Worktree>),
+ Weak(WeakModelHandle<Worktree>),
}
enum ProjectClientState {
@@ -57,12 +68,12 @@ pub struct Collaborator {
pub replica_id: ReplicaId,
}
-#[derive(Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub enum Event {
ActiveEntryChanged(Option<ProjectEntry>),
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<Buffer>,
+ pub target_range: Range<language::Anchor>,
+}
+
impl DiagnosticSummary {
fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> 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::<Vec<_>>() {
+ 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<Worktree>] {
- &self.worktrees
+ pub fn worktrees<'a>(
+ &'a self,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+ 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<ModelHandle<Worktree>> {
- self.worktrees
- .iter()
+ self.worktrees(cx)
.find(|worktree| worktree.read(cx).id() == id)
- .cloned()
}
pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
@@ -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::<Vec<_>>() {
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::<Vec<_>>() {
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<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
- 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<Buffer>,
+ abs_path: PathBuf,
+ cx: &mut ModelContext<Project>,
+ ) -> Task<Result<()>> {
+ 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<Worktree>,
+ buffer: ModelHandle<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) -> 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<Client>,
+ language: Arc<Language>,
+ worktree_path: &Path,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Arc<LanguageServer>> {
+ 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::<lsp::notification::PublishDiagnostics, _>({
+ 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::<lsp::notification::Progress, _>(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<String>,
+ cx: &mut ModelContext<Self>,
+ ) -> 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<T: ToOffset>(
+ &self,
+ source_buffer_handle: &ModelHandle<Buffer>,
+ position: T,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<Definition>>> {
+ 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::request::GotoDefinition>(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<Path>,
+ weak: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<(ModelHandle<Worktree>, 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<Self>,
+ ) -> Task<Result<(ModelHandle<Worktree>, 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<Worktree>, 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<Path>,
+ weak: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Worktree>>> {
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<Worktree>, cx: &mut ModelContext<Self>) {
+ pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext<Self>) {
+ 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<Worktree>, cx: &mut ModelContext<Self>) {
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<Item = (ProjectPath, DiagnosticSummary)> + '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>) {
+ 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<Self>) {
+ 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<ProjectEntry> {
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::<Vec<_>>() {
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<Self>,
) -> 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<proto::DiskBasedDiagnosticsUpdating>,
+ _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> 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<proto::DiskBasedDiagnosticsUpdated>,
+ _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
_: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> 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<Client>,
cx: &mut ModelContext<Self>,
) -> 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<proto::CloseBuffer>,
- rpc: Arc<Client>,
+ _: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> 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<Output = Vec<PathMatch>> {
- 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::<Vec<_>>();
+ 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<ModelHandle<Worktree>> {
+ 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<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
+ use futures::FutureExt;
+
+ let shutdown_futures = self
+ .language_servers
+ .drain()
+ .filter_map(|(_, server)| server.shutdown())
+ .collect::<Vec<_>>();
+ 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();
@@ -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<Path>),
-}
-
impl Entity for Worktree {
- type Event = Event;
+ type Event = ();
- fn app_will_quit(
- &mut self,
- _: &mut MutableAppContext,
- ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
- use futures::FutureExt;
-
- if let Self::Local(worktree) = self {
- let shutdown_futures = worktree
- .language_servers
- .drain()
- .filter_map(|(_, server)| server.shutdown())
- .collect::<Vec<_>>();
- 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<Client>,
user_store: ModelHandle<UserStore>,
path: impl Into<Arc<Path>>,
+ weak: bool,
fs: Arc<dyn Fs>,
- languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<Client>,
user_store: ModelHandle<UserStore>,
- languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<LanguageRegistry> {
- match self {
- Worktree::Local(worktree) => &worktree.language_registry,
- Worktree::Remote(worktree) => &worktree.languages,
- }
- }
-
pub fn user_store(&self) -> &ModelHandle<UserStore> {
match self {
Worktree::Local(worktree) => &worktree.user_store,
@@ -320,43 +311,6 @@ impl Worktree {
}
}
- pub fn handle_open_buffer(
- &mut self,
- envelope: TypedEnvelope<proto::OpenBuffer>,
- rpc: Arc<Client>,
- cx: &mut ModelContext<Self>,
- ) -> 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<proto::CloseBuffer>,
- _: Arc<Client>,
- cx: &mut ModelContext<Self>,
- ) -> anyhow::Result<()> {
- self.as_local_mut()
- .unwrap()
- .close_remote_buffer(envelope, cx)
- }
-
pub fn diagnostic_summaries<'a>(
&'a self,
) -> impl Iterator<Item = (Arc<Path>, DiagnosticSummary)> + 'a {
@@ -379,7 +333,7 @@ impl Worktree {
&mut self,
path: impl AsRef<Path>,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<ModelHandle<Buffer>>> {
+ ) -> Task<Result<(ModelHandle<Buffer>, 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<Path> = 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<String>,
- cx: &mut ModelContext<Worktree>,
- ) -> 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<Path>,
- version: Option<i32>,
- diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
- cx: &mut ModelContext<Self>,
- ) -> 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<ScanState>,
_background_scanner_task: Option<Task<()>>,
poll_task: Option<Task<()>>,
+ registration: Registration,
share: Option<ShareState>,
loading_buffers: LoadingBuffers,
open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
@@ -998,12 +787,17 @@ pub struct LocalWorktree {
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
queued_operations: Vec<(u64, Operation)>,
- language_registry: Arc<LanguageRegistry>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>,
- languages: Vec<Arc<Language>>,
- language_servers: HashMap<String, Arc<LanguageServer>>,
+ 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<usize, RemoteBuffer>,
- languages: Arc<LanguageRegistry>,
user_store: ModelHandle<UserStore>,
queued_operations: Vec<(u64, Operation)>,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
+ weak: bool,
}
type LoadingBuffers = HashMap<
Arc<Path>,
- postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
+ postage::watch::Receiver<
+ Option<Result<(ModelHandle<Buffer>, Arc<AtomicBool>), Arc<anyhow::Error>>>,
+ >,
>;
#[derive(Default, Deserialize)]
@@ -1042,8 +838,8 @@ impl LocalWorktree {
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
path: impl Into<Arc<Path>>,
+ weak: bool,
fs: Arc<dyn Fs>,
- languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<(ModelHandle<Worktree>, Sender<ScanState>)> {
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<Language>] {
- &self.languages
+ fn get_open_buffer(
+ &mut self,
+ path: &Path,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Option<ModelHandle<Buffer>> {
+ 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<Language>,
+ path: &Path,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Arc<LanguageServer>> {
- if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) {
- self.languages.push(language.clone());
- }
+ ) -> Task<Result<ModelHandle<Buffer>>> {
+ 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::<lsp::notification::PublishDiagnostics, _>(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::<lsp::notification::Progress, _>(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<Buffer>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> 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<proto::CloseBuffer>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> 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<Worktree>,
- ) -> Option<ModelHandle<Buffer>> {
- 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<Path>,
+ params: lsp::PublishDiagnosticsParams,
+ disk_based_sources: &HashSet<String>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> 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<Worktree>,
- ) -> Task<Result<ModelHandle<Buffer>>> {
- 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<proto::OpenBuffer>,
+ worktree_path: Arc<Path>,
+ version: Option<i32>,
+ diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<proto::OpenBufferResponse>> {
- 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<proto::CloseBuffer>,
- cx: &mut ModelContext<Worktree>,
- ) -> 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<Worktree>,
- ) {
- 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<Output = ()> {
@@ -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<Project>,
list: UniformListState,
- visible_entries: Vec<Vec<usize>>,
+ visible_entries: Vec<(WorktreeId, Vec<usize>)>,
expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
selection: Option<Selection>,
settings: watch::Receiver<Settings>,
@@ -260,7 +260,11 @@ impl ProjectPanel {
}
fn select_first(&mut self, cx: &mut ViewContext<Self>) {
- 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<Self>,
) {
- 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<C: ReadModel>(
+ fn for_each_visible_entry(
&self,
range: Range<usize>,
- cx: &mut C,
- mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C),
+ cx: &mut ViewContext<ProjectPanel>,
+ mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
) {
- 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::<Vec<_>>() {
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;
@@ -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 {
@@ -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
@@ -31,6 +31,7 @@ pub struct Worktree {
pub authorized_user_ids: Vec<UserId>,
pub root_name: String,
pub share: Option<WorktreeShare>,
+ 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::<Vec<_>>();
worktree_root_names.sort_unstable();
@@ -76,14 +76,16 @@ pub struct Pane {
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
active_item_index: usize,
settings: watch::Receiver<Settings>,
- navigation: Rc<Navigation>,
+ nav_history: Rc<RefCell<NavHistory>>,
}
-#[derive(Default)]
-pub struct Navigation(RefCell<NavigationHistory>);
+pub struct ItemNavHistory {
+ history: Rc<RefCell<NavHistory>>,
+ item_view: Rc<dyn WeakItemViewHandle>,
+}
#[derive(Default)]
-struct NavigationHistory {
+pub struct NavHistory {
mode: NavigationMode,
backward_stack: VecDeque<NavigationEntry>,
forward_stack: VecDeque<NavigationEntry>,
@@ -104,7 +106,7 @@ impl Default for NavigationMode {
}
pub struct NavigationEntry {
- pub item_view: Box<dyn WeakItemViewHandle>,
+ pub item_view: Rc<dyn WeakItemViewHandle>,
pub data: Option<Box<dyn Any>>,
}
@@ -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<Self>) {
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<NavigationEntry> {
- self.0.borrow_mut().backward_stack.pop_back()
+impl ItemNavHistory {
+ pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
+ Self {
+ history,
+ item_view: Rc::new(item_view.downgrade()),
+ }
}
- pub fn pop_forward(&self) -> Option<NavigationEntry> {
- self.0.borrow_mut().forward_stack.pop_back()
+ pub fn history(&self) -> Rc<RefCell<NavHistory>> {
+ self.history.clone()
}
- fn pop(&self, mode: NavigationMode) -> Option<NavigationEntry> {
+ pub fn push<D: 'static + Any>(&self, data: Option<D>) {
+ self.history.borrow_mut().push(data, self.item_view.clone());
+ }
+}
+
+impl NavHistory {
+ pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
+ self.backward_stack.pop_back()
+ }
+
+ pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
+ self.forward_stack.pop_back()
+ }
+
+ fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
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<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
- let mut state = self.0.borrow_mut();
- match state.mode {
+ pub fn push<D: 'static + Any>(
+ &mut self,
+ data: Option<D>,
+ item_view: Rc<dyn WeakItemViewHandle>,
+ ) {
+ 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<dyn Any>),
});
- 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<dyn Any>),
});
}
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<dyn Any>),
});
}
@@ -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>| {
+ 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<Worktree>,
+ cx: &mut ModelContext<Project>,
) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
}
@@ -137,7 +142,7 @@ pub trait Item: Entity + Sized {
fn build_view(
handle: ModelHandle<Self>,
workspace: &Workspace,
- navigation: Rc<Navigation>,
+ nav_history: ItemNavHistory,
cx: &mut ViewContext<Self::View>,
) -> 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<Self>) -> Result<Task<Result<()>>>;
+ fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
fn can_save_as(&self, cx: &AppContext) -> bool;
fn save_as(
&mut self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>>;
+ ) -> Task<Result<()>>;
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<Self>,
+ _: &AppContext,
+ ) -> Option<AnyViewHandle> {
+ if TypeId::of::<Self>() == 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<Navigation>,
+ nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle>;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -204,7 +221,7 @@ pub trait WeakItemHandle {
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
}
-pub trait ItemViewHandle {
+pub trait ItemViewHandle: 'static {
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
fn title(&self, cx: &AppContext) -> String;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -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<Task<Result<()>>>;
+ fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>>;
fn save_as(
&self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
cx: &mut MutableAppContext,
- ) -> Task<anyhow::Result<()>>;
+ ) -> Task<Result<()>>;
+ fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
}
pub trait WeakItemViewHandle {
@@ -242,11 +260,12 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
&self,
window_id: usize,
workspace: &Workspace,
- navigation: Rc<Navigation>,
+ nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> {
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<dyn ItemHandle> {
&self,
window_id: usize,
workspace: &Workspace,
- navigation: Rc<Navigation>,
+ nav_history: Rc<RefCell<NavHistory>>,
cx: &mut MutableAppContext,
) -> Box<dyn ItemViewHandle> {
- 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<dyn ItemHandle> {
@@ -323,6 +342,17 @@ impl PartialEq for Box<dyn WeakItemHandle> {
impl Eq for Box<dyn WeakItemHandle> {}
+impl dyn ItemViewHandle {
+ pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
+ self.to_any().downcast()
+ }
+
+ pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+ self.act_as_type(TypeId::of::<T>(), cx)
+ .and_then(|t| t.downcast())
+ }
+}
+
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
Box::new(self.read(cx).item_handle(cx))
@@ -374,17 +404,17 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
self.update(cx, |this, cx| this.navigate(data, cx));
}
- fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
+ fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>> {
self.update(cx, |item, cx| item.save(cx))
}
fn save_as(
&self,
- worktree: ModelHandle<Worktree>,
- path: &Path,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
cx: &mut MutableAppContext,
) -> Task<anyhow::Result<()>> {
- 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<T: ItemView> ItemViewHandle for ViewHandle<T> {
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<AnyViewHandle> {
+ self.read(cx).act_as_type(type_id, self, cx)
+ }
+}
+
+impl Into<AnyViewHandle> for Box<dyn ItemViewHandle> {
+ fn into(self) -> AnyViewHandle {
+ self.to_any()
+ }
}
impl Clone for Box<dyn ItemViewHandle> {
@@ -600,8 +640,11 @@ impl Workspace {
&self.project
}
- pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
- &self.project.read(cx).worktrees()
+ pub fn worktrees<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+ 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<Output = ()> + 'static {
let futures = self
.worktrees(cx)
- .iter()
.filter_map(|worktree| worktree.read(cx).as_local())
.map(|worktree| worktree.scan_complete())
.collect::<Vec<_>>();
@@ -675,44 +717,14 @@ impl Workspace {
})
}
- fn worktree_for_abs_path(
- &self,
- abs_path: &Path,
- cx: &mut ViewContext<Self>,
- ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
- let abs_path: Arc<Path> = 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<Self>,
) -> Task<Result<ProjectPath>> {
- 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<Self>,
- ) -> Task<Result<ModelHandle<Worktree>>> {
- self.project
- .update(cx, |project, cx| project.add_local_worktree(path, cx))
- }
-
pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, 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<Self>) {
+ pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
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<Workspace> {
fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
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(
@@ -175,7 +175,7 @@ mod tests {
assert_eq!(cx.window_ids().len(), 1);
let workspace_1 = cx.root_view::<Workspace>(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::<editor::Editor>()
.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::<HashSet<_>>();
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::<Editor>().unwrap()
+ item.downcast::<Editor>().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::<Editor>()
.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::<Editor>()
.unwrap()
});
@@ -597,7 +578,6 @@ mod tests {
workspace
.active_item(cx)
.unwrap()
- .to_any()
.downcast::<Editor>()
.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::<Editor>()
.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::<Editor>()
.unwrap();
let editor3 = workspace
.update(&mut cx, |w, cx| w.open_path(file3.clone(), cx))
.await
.unwrap()
- .to_any()
.downcast::<Editor>()
.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::<Editor>().unwrap();
+ let editor = item.downcast::<Editor>().unwrap();
let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx));
(item.project_path(cx).unwrap(), selections[0].start)
})