Merge pull request #354 from zed-industries/go-to-definition
Go to definition
This commit is contained in:
@@ -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));
|
||||
|
||||
|
||||
+107
-43
@@ -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))
|
||||
|
||||
+23
-55
@@ -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>) {
|
||||
|
||||
+161
-78
@@ -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(
|
||||
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
|
||||
self.language = language;
|
||||
self.reparse(cx);
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
+41
-39
@@ -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
|
||||
|
||||
+848
-139
File diff suppressed because it is too large
Load Diff
+540
-816
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
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);
|
||||
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 {
|
||||
|
||||
+80
-44
@@ -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>>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
history: Rc<RefCell<NavHistory>>,
|
||||
item_view: Rc<dyn WeakItemViewHandle>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Navigation(RefCell<NavigationHistory>);
|
||||
|
||||
#[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>),
|
||||
});
|
||||
}
|
||||
|
||||
+104
-134
@@ -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(
|
||||
|
||||
+54
-80
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user